/*
 * Copyright (c) 2018 Hitevision
 */

package tw.com.hitevision.whiteboard.android;

import static tw.com.hitevision.whiteboard.android.fastdraw.FastDraw.LOCK_FRAME_BUFFER_OFF;
import static tw.com.hitevision.whiteboard.android.fastdraw.FastDraw.LOCK_FRAME_BUFFER_ON;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.util.Log;
import android.util.SparseArray;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.ViewConfiguration;

import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy;
import com.bumptech.glide.request.FutureTarget;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedDeque;

import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.view.GestureDetectorCompat;

import tw.com.hitevision.whiteboard.android.display.DisplayDpiHelper;
import tw.com.hitevision.whiteboard.android.fastdraw.FastDraw;
import tw.com.hitevision.whiteboard.android.render.Autoshape;
import tw.com.hitevision.whiteboard.android.render.BaseItem;
import tw.com.hitevision.whiteboard.android.render.Eraser;
import tw.com.hitevision.whiteboard.android.render.Image;
import tw.com.hitevision.whiteboard.android.render.Line;
import tw.com.hitevision.whiteboard.android.render.ObjectEraserPen;
import tw.com.hitevision.whiteboard.android.render.PartialEraserPen;
import tw.com.hitevision.whiteboard.android.render.ProtractorItem;
import tw.com.hitevision.whiteboard.android.render.RulerHelper;
import tw.com.hitevision.whiteboard.android.render.SelectionBox;
import tw.com.hitevision.whiteboard.android.render.Selector;
import tw.com.hitevision.whiteboard.android.render.StickyNote;
import tw.com.hitevision.whiteboard.android.render.Word;
import tw.com.hitevision.whiteboard.android.state.Action;
import tw.com.hitevision.whiteboard.android.state.AutoshapeProperty;
import tw.com.hitevision.whiteboard.android.state.HighlighterProperty;
import tw.com.hitevision.whiteboard.android.state.LineProperty;
import tw.com.hitevision.whiteboard.android.state.PageProperty;
import tw.com.hitevision.whiteboard.android.state.PenProperty;
import tw.com.hitevision.whiteboard.android.state.Transform;
import tw.com.hitevision.whiteboard.android.state.WhiteboardState;
import tw.com.hitevision.whiteboard.android.state.WordProperty;
import tw.com.hitevision.whiteboard.android.stateFile.JsonHandler;
import tw.com.hitevision.whiteboard.android.storage.FileHandler;
import tw.com.hitevision.whiteboard.android.util.BroadcastHelper;
import tw.com.hitevision.whiteboard.android.util.FlingHelper;
import tw.com.hitevision.whiteboard.android.util.GlideApp;
import tw.com.hitevision.whiteboard.android.util.InputManager;
import tw.com.hitevision.whiteboard.android.util.SystemPropertiesUtils;
import tw.com.hitevision.whiteboard.android.util.TouchUtils;
import tw.com.hitevision.whiteboard.android.widgets.MeasurementTools.ProtractorPaintMode;
import tw.com.hitevision.whiteboard.android.widgets.SearchImageView;

public class Whiteboard implements RenderThread.Renderer {
    private static final String TAG = Whiteboard.class.getSimpleName();

    /**
     * The maximum number of input pointers.
     */
    private static final int MAX_INPUT_POINTERS = 20;

    private static final long ADD_TEXT_BLINK_TIME = 3000L; // in millseconds

    private static final long MAX_TIME_INTERVAL_MULTI_FINGER_GESTURE = 200;

    public static final float ERASER_SIZE = Props.isFake4k() ? 104 : 52;
    private static final float ERASER_SIZE_PALM = 64;
    private static final long TIME_INTERVAL_DO_ERASER = 1250L;

    private static final int TRIMMING_MIN_SIZE = 130;

    private static final float HINT_ADD_TEXT_ALPHA = 0.7f;

    protected static final String CLEAR_TEXT_SELECTION_AREA_ACTION = "CLEAR_TEXT_SELECTION_AREA";
    protected static final String RESET_TEXT_SELECTION_AREA_ACTION = "RESET_TEXT_SELECTION_AREA";

    private static final long TIMEOUT_RENDER_PREVIEW = 3 * 1000; // in ms
    private static final long TIMEOUT_TOUCH_EVENT = 5000;

    private static final int INVALID_POINTER_ID = -1;

    private static final int TOTAL_CLEAN_FRAME_BUFFER_COUNT = 10;
    @ColorInt
    private static final int CLEAN_FRAME_BUFFER_COLOR_LIGHT = 0x02333333;
    private static final int CLEAN_FRAME_BUFFER_COLOR_DARK = 0x02cccccc;

    private static final float MIN_FLING_VELOCITY = 1000.0f;

    private static final float MAX_ZOOM = 3.0f;
    private static final float MIN_ZOOM = 0.75f;

    private final float minItemSize;
    private final float eraserPadding;
    private final float scrollGridSpacing;
    @ColorInt
    private final int scrollGridColorLight;
    @ColorInt
    private final int scrollGridColorDark;
    private final int previewFileWidth;
    private final int previewFileHeight;

    private final float hintTextSize;
    @ColorInt
    private final int hintTextColorDark;
    @ColorInt
    private final int hintTextColorLight;

    private final Context context;
    private final Context baseContext;
    private final FileHandler fileHandler;
    private final FastDraw fastDraw;

    private final Paint paint;
    private final Paint transparentPaint;
    private final Paint coverPaint;

    private final Canvas offscreenCanvas;
    private Bitmap cached;
    private Bitmap eraserScratchCached;
    private Bitmap eraserBackground;
    private Deque<BaseItem> cacheQueue;

    private PenProperty fingerPenProperty;
    private PenProperty stylusPenProperty;
    private HighlighterProperty fingerHighlighterProperty;
    private HighlighterProperty stylusHighlighterProperty;
    private AutoshapeProperty autoshapeProperty;
    private WordProperty wordProperty;
    public static boolean first = true;
    private int width = 1;
    private int height = 1;
    public static boolean isscale = true;
    private Listener listener;
    private Device[] pointerDevices;
    private Input[] pointerInputs;
    private InputManager inputManager;

    private int fingerActiveCount;
    private SparseArray<BaseItem> activeItems;
    private int renderPatternRes;
    private Drawable renderPatternDrawable;

    private PointF[] lastEvents;
    private PointF[] downPoints;
    private PointF[] dragPoints;
    private PointF[] lastDragPoints;
    private PointF tempPointF;
    private PointF copyPointF;
    private PointF annotationCopyPointF;
    private List<BaseItem> scrapbook;
    private List<BaseItem> annotationScrapbook;

    private PointF oldLength;
    private PointF newLength;

    private boolean doEraserFlag;
    private long doEraserTimer;
    private int updateDragCounter;

    private Selector selectionLine;
    private Eraser eraserArea;
    private Region eraserRegion;
    private Region tempRegion;
    private boolean isMultiFingerActive;
    private boolean isMultiTouchAllowed;
    private int multiFingerCount;
    private long firstFingerDownTime;
    private PointF fiveFingerEraserCenter;
    private float eraserSize;
    private boolean inTouchSlop;
    private boolean isPartialEraserDoing;
    private boolean isObjectEraserDoing;
    private boolean notifyEraserDone = false;

    private boolean isTrimmingActive;

    private WhiteboardState state;

    private SelectionBox selectionBox;
    private final Region selectRegion;
    private List<BaseItem> selectedItems;
    private SelectionMode selectionMode;
    private Transform transform;
    private float rotateAngleOffset;
    private int selectionControl;

    private PointF[] pointsArray;

    @ColorRes
    private int lastPageColorRes;
    @ColorInt
    private int lastPageColor;
    private boolean isLastPageColorDark;

    private volatile boolean doRenderCached;
    private volatile boolean doRenderSelectionPreview;

    private Word activeWord;
    /**
     * Starting blink time for add text hint.
     */
    private long startBlinkTime;
    private boolean showTapScreen;
    private boolean showTapScreenStickynote;
    private final String hintAddText;
    private final String hintAddStickynote;
    private float hintAddTextLength;
    private float hintAddStickynoteLength;
    private String hintText;
    private float hintTextLength;
    @ColorInt
    private int hintAddTextColorDark;
    @ColorInt
    private int hintAddTextColorLight;

    //    private Deque<InputEvent> bufferQueue;
    private ConcurrentLinkedDeque<InputEvent> bufferQueue;
    private Deque<InputEvent> renderQueue;
    private final Object inputLock = new Object();

    private Device currentDevice;
    private Input previousFingerInput;
    private Input previousStylusInput;

    private List<List<PointF>> pointsList;

    private long lastStateChangeTime;
    private long lastTouchEventTime;

    private List<Integer> tempErasedIndexes;
    private List<BaseItem> tempErasedStates;

    private RenderThread.Invalidator invalidator;

    private int eraserPointerId;
    private int selectionPointerId;

    private int cleanFrameBufferCount = 0;

    private int scrollPointerId;
    private float scrollX;
    private float scrollY;
    private float[] scrollbgXs;
    private float[] scrollbgYs;
    private float[] bgzoomScales;
    private float scrollGridStartX;
    private float scrollGridStartY;

    private float appliedScale;
    private float zoomScale;
    private final PointF zoomPoint;
    private final PointF zoomDownPoint;

    private final GestureDetectorCompat gestureDetector;
    private final FlingHelper flingHelper;
    private boolean isFlinging;
    private long scrollFlingStart;

    private RectF itemsRect;
    private int backgroundX;
    private int backgroundY;

    private PartialEraserPen partialEraserPen;
    private ObjectEraserPen objectEraserPen;

    private final Matrix scrollMatrix;
    private final float[] tempPair;

    private boolean showHintText;
    private boolean showHintStickynote;

    private final float touchSlopDistance;

    private PointF[] zDownPoints;
    private PointF[] zDragPoints;
    private PointF[] zLastDragPoints;
    private boolean justTouchUp;

    private JsonHandler jsonHandler;

    private Canvas fastDrawEraserCanvas = null;
    private boolean clearWhiteBoard = false;
    private final String sharePreference;

    private ArrayList<Map<String, Float>> keyFrameBufferLockOffAppWriteActionMoveCache;

    public ArrayList<Map<String, Float>> getKeyFrameBufferLockOffAppWriteActionMoveCache() {
        return keyFrameBufferLockOffAppWriteActionMoveCache;
    }

    private RulerHelper rulerHelper;
    private ProtractorItem protractorItem;

    public Whiteboard(Context context, FileHandler fileHandler, FastDraw fastDraw, JsonHandler jsonHandler,
                      WhiteboardState state, Context baseContext) {
        this.context = context.getApplicationContext();
        this.baseContext = baseContext;
        this.fileHandler = fileHandler;
        this.fastDraw = fastDraw;
        this.jsonHandler = jsonHandler;
        this.state = state;
        this.first = true;
        this.isscale = true;
        sharePreference = context.getString(R.string.preferences_name);
        final Resources res = context.getResources();
        minItemSize = res.getDimensionPixelSize(R.dimen.item_min_size);
        eraserPadding = res.getDimensionPixelSize(R.dimen.eraser_padding);
        scrollGridSpacing = res.getDimensionPixelSize(R.dimen.scroll_grid_spacing);
        scrollGridColorLight = ContextCompat.getColor(context, R.color.scroll_grid_color_light);
        scrollGridColorDark = ContextCompat.getColor(context, R.color.scroll_grid_color_dark);
        previewFileWidth = res.getDimensionPixelSize(R.dimen.preview_file_width);
        previewFileHeight = res.getDimensionPixelSize(R.dimen.preview_file_height);

        hintTextSize = res.getDimensionPixelSize(R.dimen.hint_text_size);
        hintTextColorLight = ContextCompat.getColor(context, R.color.hint_text_light);
        hintTextColorDark = ContextCompat.getColor(context, R.color.hint_text_dark);

        gestureDetector = new GestureDetectorCompat(context, gestureListener);
        flingHelper = new FlingHelper(0.05f);
//        flingHelper = new FlingHelper(0.05f);

        itemsRect = new RectF();

        paint = new Paint();
        paint.setFlags(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG | Paint.LINEAR_TEXT_FLAG);
        transparentPaint = new Paint();
        transparentPaint.setColor(res.getColor(android.R.color.transparent));
        transparentPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
        coverPaint = new Paint();

        coverPaint.setColor(ContextCompat.getColor(context, R.color.highlight_white));
        coverPaint.setAlpha(0x32);

        offscreenCanvas = new Canvas();
        cacheQueue = new ArrayDeque<>();

        pointerDevices = new Device[MAX_INPUT_POINTERS];

        inputManager = InputManager.getInstance();

        pointsList = new ArrayList<>(MAX_INPUT_POINTERS);
        for (int i = 0; i < MAX_INPUT_POINTERS; i++) {
            pointsList.add(new ArrayList<>());
        }

        eraserPointerId = INVALID_POINTER_ID;
        selectionPointerId = INVALID_POINTER_ID;
        scrollPointerId = INVALID_POINTER_ID;

        lastEvents = new PointF[MAX_INPUT_POINTERS];
        downPoints = new PointF[MAX_INPUT_POINTERS];
        dragPoints = new PointF[MAX_INPUT_POINTERS];
        lastDragPoints = new PointF[MAX_INPUT_POINTERS];
        for (int i = 0; i < MAX_INPUT_POINTERS; i++) {
            downPoints[i] = new PointF();
            lastDragPoints[i] = new PointF();
            dragPoints[i] = new PointF();
        }
        tempPointF = new PointF();
        copyPointF = new PointF();
        annotationCopyPointF = new PointF();
        scrapbook = new ArrayList<>();
        annotationScrapbook = new ArrayList<>();
        doEraserFlag = true;

        appliedScale = 1;
        zoomScale = 1;

        bgzoomScales = new float[]{
                1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
        };
        scrollbgXs = new float[]{
                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
        };
        scrollbgYs = new float[]{
                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
        };

        zoomPoint = new PointF();
        zoomDownPoint = new PointF();

        scrollMatrix = new Matrix();
        tempPair = new float[2];

        oldLength = new PointF();
        newLength = new PointF();
        pointsArray = new PointF[5];
        for (int i = 0; i < 5; i++) {
            pointsArray[i] = new PointF();
        }
        updateDragCounter = 0;
        isPartialEraserDoing = false;
        isObjectEraserDoing = false;

        selectionLine = new Selector(context, 0, 0, 4.0f);
        eraserArea = new Eraser(context, -30, -30, AutoshapeProperty.Type.CIRCLE, true);
        eraserRegion = new Region();
        tempRegion = new Region();

        eraserArea.setVisible(false);

        fiveFingerEraserCenter = new PointF();
        isMultiFingerActive = false;
        isMultiTouchAllowed = true;
        doEraserTimer = System.currentTimeMillis();
//        eraserSize = ERASER_SIZE;
        eraserSize = getEraserSize(Device.FINGER);

        pointerInputs = new Input[MAX_INPUT_POINTERS];
        activeItems = new SparseArray<>(MAX_INPUT_POINTERS);

        hintAddTextColorDark = ContextCompat.getColor(context, R.color.hint_add_text_dark);
        hintAddTextColorLight = ContextCompat.getColor(context, R.color.hint_add_text_light);
        showTapScreen = false;
        showTapScreenStickynote = false;
        startBlinkTime = 0;

        hintAddText = context.getResources().getString(R.string.hint_add_text);
        hintAddStickynote = context.getResources().getString(R.string.hint_add_stickynote);
        paint.setTextSize(hintTextSize);
        hintAddTextLength = paint.measureText(hintAddText);
        hintAddStickynoteLength = paint.measureText(hintAddStickynote);

        selectionBox = new SelectionBox(context);
        selectRegion = new Region();
        selectedItems = new ArrayList<>();
        selectionMode = SelectionMode.NONE;
        transform = new Transform(context);

//        bufferQueue = new ArrayDeque<>();
        bufferQueue = new ConcurrentLinkedDeque<>();
        renderQueue = new ArrayDeque<>();

        currentDevice = Device.FINGER;
        previousFingerInput = Input.HIGHLIGHTER;
        previousStylusInput = Input.PEN;

        tempErasedIndexes = new ArrayList<>();
        tempErasedStates = new ArrayList<>();

        lastTouchEventTime = Long.MAX_VALUE;

        resetAutomaticPreviewUpdate();

        float touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        touchSlopDistance = touchSlop * touchSlop * 0.2f;

        zDownPoints = new PointF[MAX_INPUT_POINTERS];
        zDragPoints = new PointF[MAX_INPUT_POINTERS];
        zLastDragPoints = new PointF[MAX_INPUT_POINTERS];
        for (int i = 0; i < MAX_INPUT_POINTERS; i++) {
            zDownPoints[i] = new PointF();
            zDragPoints[i] = new PointF();
            zLastDragPoints[i] = new PointF();
        }

        justTouchUp = false;

        if (FastDraw.isSupportedCanvasFastDrawEraser()) {
            fastDrawEraserCanvas = new Canvas();
        }

        keyFrameBufferLockOffAppWriteActionMoveCache = new ArrayList<>();

    }

    public void setListener(Listener listener) {
        this.listener = listener;
    }

    private void setState(WhiteboardState state) {
        if (this.state != null) {
            if (!this.state.isAnnotationState() && this.state.isPreviewDirty()) {
                runRenderStatePreview(this.state);
            }

            for (BaseItem item : this.state.getItems()) {
                item.release(context);
                item.setSelected(false);
            }

            this.state.setAppliedScale(appliedScale);
        }

        activeItems.clear();

        if (this.state.isAnnotationState() != state.isAnnotationState()) {
            inputManager.setStateMode(state.isAnnotationState());
        }

        this.state.setBackgroundPosition(backgroundX, backgroundY);

        this.state = state;

        backgroundX = state.getBackgroundX();
        backgroundY = state.getBackgroundY();

        appliedScale = state.getAppliedScale();

        update();
    }

    private void setCenteredBounds(Drawable drawable) {
        final int w = drawable.getIntrinsicWidth();
        final int h = drawable.getIntrinsicHeight();

        boolean isDefault = (state.getPagePatternRes() != -1);

//        if (w <= width && h <= height && !isDefault) {
//            // center
//            drawable.setBounds((int) (((scrollbgX + backgroundX + (width - w)) / 2) * zoomScale), (int) (((scrollbgY + backgroundY + (height - h)) / 2) * zoomScale),
//                    (int) (((scrollbgX + backgroundX + (width + w)) / 2) * zoomScale), (int) (((scrollbgY + backgroundY + (height + h)) / 2) * zoomScale));
//        } else {
//            final float drawableRatio = 1.0f * w / h;
//            final float whiteboardRatio = 1.0f * width / height;
//            Log.d("zf", "setCenteredBounds: zoomscale is " + zoomScale);
//            if (isscale) {
//                if (drawableRatio > whiteboardRatio) {
//                    final int scaledH = (int) (width / drawableRatio);
//                    drawable.setBounds((int) ((scrollbgX + backgroundX) * zoomScale), (int) (((scrollbgY + backgroundY + (height - scaledH)) / 2) * zoomScale),
//                            (int) ((scrollbgX + backgroundX + width) * zoomScale), (int) (((scrollbgY + backgroundY + (height + scaledH) / 2)) * zoomScale));
//                } else {
//                    final int scaledW = (int) (drawableRatio * height);
//                    drawable.setBounds((int) (((scrollbgX + backgroundX + (width - scaledW)) / 2) * zoomScale), (int) ((scrollbgY + backgroundY) * zoomScale),
//                            (int) ((scrollbgX + backgroundX + (width + scaledW) / 2) * zoomScale), (int) (((scrollbgY + backgroundY + height)) * zoomScale));
//                }
//            } else {
//
//            }
//            // center scaled
//        }
        if (w <= width && h <= height && !isDefault) {
            // center
            drawable.setBounds((int) ((scrollbgXs[MainActivity.stateIndex] + backgroundX + width) * bgzoomScales[MainActivity.stateIndex]), (int) ((scrollbgYs[MainActivity.stateIndex] + backgroundY + (height - h) / 2) * bgzoomScales[MainActivity.stateIndex]),
                    (int) ((scrollbgXs[MainActivity.stateIndex] + backgroundX + (width + w) / 2) * bgzoomScales[MainActivity.stateIndex]), (int) ((scrollbgYs[MainActivity.stateIndex] + backgroundY + (height + h) / 2) * bgzoomScales[MainActivity.stateIndex]));
        } else {
            final float drawableRatio = 1.0f * w / h;
            final float whiteboardRatio = 1.0f * width / height;
            Log.d("zf", "setCenteredBounds: zoomscale is " + zoomScale);
            if (isscale) {
                if (drawableRatio > whiteboardRatio) {
                    final int scaledH = (int) (width / drawableRatio);
                    drawable.setBounds((int) ((scrollbgXs[MainActivity.stateIndex] + backgroundX) * bgzoomScales[MainActivity.stateIndex]), (int) ((scrollbgYs[MainActivity.stateIndex] + backgroundY + (height - scaledH) / 2) * bgzoomScales[MainActivity.stateIndex]),
                            (int) ((scrollbgXs[MainActivity.stateIndex] + backgroundX + width) * bgzoomScales[MainActivity.stateIndex]), (int) ((scrollbgYs[MainActivity.stateIndex] + backgroundY + (height + scaledH) / 2) * bgzoomScales[MainActivity.stateIndex]));
                } else {
                    final int scaledW = (int) (drawableRatio * height);
                    drawable.setBounds((int) ((scrollbgXs[MainActivity.stateIndex] + backgroundX + (width - scaledW) / 2) * bgzoomScales[MainActivity.stateIndex]), (int) ((scrollbgYs[MainActivity.stateIndex] + backgroundY) * bgzoomScales[MainActivity.stateIndex]),
                            (int) ((scrollbgXs[MainActivity.stateIndex] + backgroundX + (width + scaledW) / 2) * bgzoomScales[MainActivity.stateIndex]), (int) (((scrollbgYs[MainActivity.stateIndex] + backgroundY + height)) * bgzoomScales[MainActivity.stateIndex]));
                }
            } else {

            }
            // center scaled
        }
    }

    private void setExtendedBounds(Drawable drawable) {
        final int w = drawable.getIntrinsicWidth();
        final int h = drawable.getIntrinsicHeight();

//        if (w <= width && h <= height) {
//             center
//            drawable.setBounds(backgroundX + (width - w) / 2, backgroundY + (height - h) / 2,
//                    backgroundX + (width + w) / 2, backgroundY + (height + h) / 2);
//        } else {
        // center scaled
        final float drawableRatio = 1.0f * w / h;
        final float whiteboardRatio = 1.0f * width / height;
        if (drawableRatio > whiteboardRatio) {
            final int scaledH = (int) (width / drawableRatio);
            drawable.setBounds(backgroundX, backgroundY + (height - scaledH) / 2,
                    backgroundX + width, backgroundY + (height + scaledH) / 2);
        } else {
            final int scaledW = (int) (drawableRatio * height);
            drawable.setBounds(backgroundX + (width - scaledW) / 2, backgroundY,
                    backgroundX + (width + scaledW) / 2, backgroundY + height);
        }
//        }

    }

//    public void setSize(int width, int height) {
//        this.width = width;
//        this.height = height;
//
//        // set bitmap for render cache
//
//        final BitmapPool pool = GlideApp.get(context).getBitmapPool();
//        if (cached != null) {
//            pool.put(cached);
//        }
//        if (eraserBackgroundCached != null) {
//            pool.put(eraserBackgroundCached);
//        }
//
//        cached = pool.getDirty(width, height, Bitmap.Config.ARGB_8888);
//        eraserBackgroundCached = pool.getDirty(width, height, Bitmap.Config.ARGB_8888);
//        // get drawable for page pattern
//
//        if (renderPatternDrawable == null && state != null && state.hasPagePattern()) {
//            if (state.isPageCustomBackgroundSet()) {
//                renderPatternDrawable = state.getPageCustomBackground();
//                setCenteredBounds(renderPatternDrawable);
//            } else {
//                if(state.getPagePatternRes() == -1) {
//                    // fail load
//                    renderPatternDrawable = ContextCompat.getDrawable(context, PageProperty.Pattern.BLANK.ordinal());
//                } else {
//                    renderPatternDrawable = ContextCompat.getDrawable(context, state.getPagePatternRes());
//                }
//            }
//            renderPatternRes = state.getPagePatternRes();
//        }
//
//        if (renderPatternDrawable != null && !state.isPageCustomBackgroundSet()) {
//            renderPatternDrawable.setBounds(0, 0, width, height);
//        }
//
//        renderCached();
//        invalidateRender();
//    }

    private void updateByDpi(int width, int height) {
        if (Props.isFake4k()) {
            DisplayDpiHelper.getInstance().setFactor(width, height);
        }


//        if (selectionBox != null) {
//            selectionBox.updateByDpi();
//        }

//        if (listener != null) {
//            listener.updateViewByDpi();
//        }

    }

    public void setPenProperties(PenProperty fingerPenProperty, PenProperty stylusPenProperty) {
        this.fingerPenProperty = fingerPenProperty;
        this.stylusPenProperty = stylusPenProperty;
    }

    public void setHighlighterProperties(HighlighterProperty fingerHighlighterProperty, HighlighterProperty stylusHighlighterProperty) {
        this.fingerHighlighterProperty = fingerHighlighterProperty;
        this.stylusHighlighterProperty = stylusHighlighterProperty;
    }

    public void setAutoshapeProperty(AutoshapeProperty autoshapeProperty) {
        this.autoshapeProperty = autoshapeProperty;
    }

    public void setWordProperty(WordProperty wordProperty) {
        this.wordProperty = wordProperty;
    }

    public WhiteboardState getState() {
        return state;
    }

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }

    private void addImage(Bitmap bitmap) {
        if (state == null || bitmap == null) {
            return;
        }
        state.setPreviewDirty(true);
        resetAutomaticPreviewUpdate();

        final float bitmapWidth = bitmap.getWidth();
        final float bitmapHeight = bitmap.getHeight();
        final float maxWidth = width / 2.0f;
        final float maxHeight = height / 2.0f;

        float scale = 1;
        if (bitmapWidth > maxWidth || bitmapHeight > maxHeight) {
            scale = Math.min(maxWidth / bitmapWidth, (maxHeight / bitmapHeight));
        }
        final float scaledWidth = bitmap.getWidth() * scale;
        final float scaledHeight = bitmap.getHeight() * scale;
        final float x = (width - scaledWidth) / 2;
        final float y = (height - scaledHeight) / 2;

        final Image image = new Image(bitmap);
        image.setBounds(x, y, scaledWidth, scaledHeight);
        image.updateBounds();
        image.setSelected(true);

        state.clearSelected();
        state.addItem(image);

        Action action = Action.createInsertAction(image.getId());
        state.addAction(action);

        selectedItems.clear();
        selectedItems.add(image);

        selectionBox.setOutlineVisible(true);
        selectionBox.setStickyNoteSelected(false);
        selectionBox.setSelectionRenderMode(SelectionBox.ButtonMode.TRIMMING);
        updateSelectionBox();

        renderSelectionPreview();
        renderCached();
        // show trimming button
        if (listener != null) {
            listener.onSelectionBoxVisibleChange(true, selectionBox.getSelectionRenderMode());
        }

        // switch input to select
        onInputChange(currentDevice, Input.SELECT);// switch input to select

    }

    /**
     * Adds an image to the whiteboard via urls.
     */
//    private void addImage(String thumbnailUrl, String sourceUrl, float newX, float newY) {
//    private void addImage(String resultUrl, float newX, float newY) {
    private void addImage(String resultUrl, float newX, float newY) {
        if (listener == null || state == null) {
            return;
        } else if (resultUrl == null || resultUrl.isEmpty()) {
            listener.disableImageSearchTouchBlock();
            return;
        }

        state.setPreviewDirty(true);
        resetAutomaticPreviewUpdate();

        FutureTarget<Bitmap> target;
        Bitmap bitmap;

        RequestListener<Bitmap> resultRequestListener = new RequestListener<Bitmap>() {
            @Override
            public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Bitmap> target, boolean isFirstResource) {
                listener.disableImageSearchTouchBlock();
                return false;
            }

            @Override
            public boolean onResourceReady(Bitmap resource, Object model, Target<Bitmap> target, DataSource dataSource, boolean isFirstResource) {
                listener.disableImageSearchTouchBlock();
                return false;
            }
        };

        try {
            // download image from source url.
            target = GlideApp.with(context)
                    .asBitmap()
                    .downsample(DownsampleStrategy.CENTER_INSIDE)
                    .listener(resultRequestListener)
                    .load(resultUrl)
                    .timeout(5000)
                    .submit((int) (width * 0.75f), (int) (height * 0.75f));

            bitmap = target.get();
        } catch (Exception e) {
            e.printStackTrace();
            listener.disableImageSearchTouchBlock();
            return;
        }

        if (bitmap == null) {
            listener.disableImageSearchTouchBlock();
            return;
        }

        if (listener != null) {
            listener.cancelSearchTaskAfterAddImage();
        }

        final float bitmapWidth = bitmap.getWidth();
        final float bitmapHeight = bitmap.getHeight();
        final float maxWidth = width / 2;
        final float maxHeight = height / 2;

        float scale = 1;
        if (bitmapWidth > maxWidth || bitmapHeight > maxHeight) {
            scale = Math.min(maxWidth / bitmapWidth, (maxHeight / bitmapHeight));
        }
        final float scaledWidth = bitmap.getWidth() * scale;
        final float scaledHeight = bitmap.getHeight() * scale;
        final float x, y;
        if (newX <= SearchImageView.INVALID_POSITION && newY <= SearchImageView.INVALID_POSITION) {
            x = (width - scaledWidth) / 2;
            y = (height - scaledHeight) / 2;
        } else {
            x = newX;
            y = newY;
        }

//        final Image image = new Image(bitmap);
        final Image image = new Image(resultUrl, bitmapWidth, bitmapHeight);
        image.setBounds(x, y, scaledWidth, scaledHeight);
        image.updateBounds();
        image.setSelected(true);

        state.clearSelected();
        cacheQueue.add(image);
        state.addItem(image);

        Action action = Action.createInsertAction(image.getId());
        state.addAction(action);

        selectedItems.clear();
        selectedItems.add(image);

        selectionBox.setOutlineVisible(true);
        selectionBox.setStickyNoteSelected(false);
        selectionBox.setSelectionRenderMode(SelectionBox.ButtonMode.TRIMMING);

        jsonHandler.sendSaveJsonImageToInternal(bitmap, image.getImageFileName());

        updateSelectionBox();
        renderSelectionPreview();
        renderCached();

        // show trimming button
        if (listener != null) {
            listener.onSelectionBoxVisibleChange(true, selectionBox.getSelectionRenderMode());
            listener.onCurrentInputChange(currentDevice, Input.SELECT);
        }

        // switch input to select
        onInputChange(currentDevice, Input.SELECT);

        GlideApp.with(context).clear(target);

    }

    private void addImage(Uri contentUri) {
        if (state == null || contentUri == null) {
            return;
        }
        state.setPreviewDirty(true);
        resetAutomaticPreviewUpdate();

        FutureTarget<Bitmap> target;
        Bitmap bitmap;
        try {
            target = GlideApp.with(context)
                    .asBitmap()
                    .downsample(DownsampleStrategy.CENTER_INSIDE)
                    .load(contentUri)
                    .submit((int) (width * 0.75f), (int) (height * 0.75f));
            bitmap = target.get();
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }
        state.setPreviewDirty(true);
        resetAutomaticPreviewUpdate();

        final float bitmapWidth = bitmap.getWidth();
        final float bitmapHeight = bitmap.getHeight();
        final float maxWidth = width / 2.0f;
        final float maxHeight = height / 2.0f;

        float scale = 1;
        if (bitmapWidth > maxWidth || bitmapHeight > maxHeight) {
            scale = Math.min(maxWidth / bitmapWidth, (maxHeight / bitmapHeight));
        }
        final float scaledWidth = bitmap.getWidth() * scale;
        final float scaledHeight = bitmap.getHeight() * scale;
        final float x = (width - scaledWidth) / 2;
        final float y = (height - scaledHeight) / 2;

        final Image image = new Image(contentUri, bitmapWidth, bitmapHeight);
        image.setBounds(x, y, scaledWidth, scaledHeight);
        image.updateBounds();
        image.setSelected(true);

        state.clearSelected();
        cacheQueue.add(image);
        state.addItem(image);

        Action action = Action.createInsertAction(image.getId());
        state.addAction(action);

        log(" addImage.onInputChange");
        // switch input to select
        onInputChange(currentDevice, Input.SELECT);
        selectedItems.clear();
        selectedItems.add(image);

        selectionBox.setOutlineVisible(true);
        selectionBox.setStickyNoteSelected(false);
        selectionBox.setSelectionRenderMode(SelectionBox.ButtonMode.TRIMMING);
        updateSelectionBox();

        jsonHandler.sendSaveJsonImageToInternal(bitmap, image.getImageFileName());

        renderSelectionPreview();
        renderCached();
        // show trimming button
        if (listener != null) {
            listener.onSelectionBoxVisibleChange(true, selectionBox.getSelectionRenderMode());
        }

        GlideApp.with(context).clear(target);
    }

    /**
     * add trimmed image
     */
    private void addTrimmedImage(Bitmap bitmap, float dx, float dy) {
        if (state == null || bitmap == null) {
            return;
        }

        final float scaledWidth = bitmap.getWidth();
        final float scaledHeight = bitmap.getHeight();
        final float x = dx;
        final float y = dy;

        final Image image = new Image(bitmap);

        int[] deleteIds = new int[selectedItems.size()];
        for (int i = 0; i < state.getItems().size(); i++) {
            if (state.getItems().get(i).isSelected()) {
                deleteIds[0] = i;
                break;
            }
        }

        image.setBounds(x, y, scaledWidth, scaledHeight);
        image.updateBounds();
        image.setSelected(true);

        state.clearSelected();
        state.addItem(image);

        long[] id = new long[]{image.getId()};
        Action action = Action.createTrimmingAction(id, selectedItems, deleteIds);
        state.addAction(action);

        state.getItems().remove(selectedItems.get(0));
        selectedItems.clear();
        selectedItems.add(image);

        selectionBox.setOutlineVisible(true);
        selectionBox.setStickyNoteSelected(false);
        selectionBox.setSelectionRenderMode(SelectionBox.ButtonMode.TRIMMING);
        updateSelectionBox();

        renderSelectionPreview();
        renderCached();
        // show trimming button
        if (listener != null) {
            listener.onSelectionBoxVisibleChange(true, selectionBox.getSelectionRenderMode());
        }

        jsonHandler.sendSaveJsonImageToInternal(bitmap, image.getImageFileName());

    }

    /**
     * Queue input event for processing.
     */
    public void queueInput(InputEvent input) {
        bufferQueue.add(input);
    }

    /**
     * Swaps the buffer and render input event queues.
     */
//    private void swapInputQueues() {
//        synchronized (inputLock) {
//            if (bufferQueue.isEmpty()) {
//                return;
//            }
//
//            Deque<InputEvent> tmp = bufferQueue;
//            bufferQueue = renderQueue;
//            renderQueue = tmp;
//        }
//    }

    /**
     * Processes events in the input queue.
     */
    private void processInputQueue(Deque<InputEvent> inputs) {
        synchronized (inputLock) {
            InputEvent event;
            while ((event = inputs.poll()) != null) {
                processInput(event);
            }
        }
    }

    /**
     * Processes an input event.
     */
    private void processInput(InputEvent input) {
        switch (input.getAction()) {
            case InputEvent.Action.CLEAR_TEXTBOX_CONTROL_KEYBOARD: {
                clearTextBoxAndToggleKeyboard();
                invalidateRender();
                break;
            }
            case InputEvent.Action.CLEAR_SELECTION: {
                clearSelectedItems();
                invalidateRender();
                break;
            }
            case InputEvent.Action.COPY: {
                List<BaseItem> clipboard = (state.isAnnotationState() ? annotationScrapbook : scrapbook);
                PointF p = (state.isAnnotationState() ? annotationCopyPointF : copyPointF);
                clipboard.clear();
                for (BaseItem baseItem : selectedItems) {
                    clipboard.add(baseItem.copy());
                }
                p.set(selectionBox.getX(), selectionBox.getY());
                clearSelectedItems();
                invalidateRender();
                break;
            }
            case InputEvent.Action.CUT: {
                List<BaseItem> clipboard = (state.isAnnotationState() ? annotationScrapbook : scrapbook);
                PointF p = (state.isAnnotationState() ? annotationCopyPointF : copyPointF);
                clipboard.clear();
//                for (BaseItem baseItem : selectedItems) {
//                    clipboard.add(baseItem.copy());
//                }
                for (int i = 0; i < selectedItems.size(); i++) {
                    clipboard.add(selectedItems.get(i).copy());
                }
                p.set(selectionBox.getX(), selectionBox.getY());
                int counter = 0;
                int[] deletedIndexes = new int[selectedItems.size()];
                for (int i = 0; i < state.getItems().size(); i++) {
                    if (state.getItems().get(i).isSelected()) {
                        deletedIndexes[counter++] = i;
                    }
                }

                Action action = Action.createDeleteAction(deletedIndexes, selectedItems);
                state.addAction(action);

                state.removeItems(deletedIndexes);
                selectedItems.clear();
                updateSelectionBox();

                state.setPreviewDirty(true);
                resetAutomaticPreviewUpdate();
                renderCached();
                invalidateRender();
                break;
            }
            case InputEvent.Action.DELETE: {
                int counter = 0;
                int[] deleteIds = new int[selectedItems.size()];
                for (int i = 0; i < state.getItems().size(); i++) {
                    if (state.getItems().get(i).isSelected()) {
                        deleteIds[counter++] = i;
                    }
                }
                Action action = Action.createDeleteAction(deleteIds, selectedItems);
                state.addAction(action);

                state.removeItems(deleteIds);
                selectedItems.clear();
                updateSelectionBox();

                state.setPreviewDirty(true);
                resetAutomaticPreviewUpdate();
                renderCached();
                invalidateRender();
                break;
            }
            case InputEvent.Action.PASTE: {
                clearSelectedItems();
                final float x = input.getX();
                final float y = input.getY();

                List<BaseItem> clipboard = (state.isAnnotationState() ? annotationScrapbook : scrapbook);
                PointF p = (state.isAnnotationState() ? annotationCopyPointF : copyPointF);
                int current = state.getItems().size();
                int pasteLength = clipboard.size();


                final long[] insertedIds = state.pasteOffset(clipboard, x - p.x, y - p.y);
                Action action = Action.createInsertAction(insertedIds);
                state.addAction(action);

                int stickyCounter = 0;
                for (int i = 0; i < pasteLength; i++) {
                    BaseItem baseItem = state.getItems().get(current + i);


                    if (baseItem != null) {
                        baseItem.setSelected(true);
                        selectedItems.add(baseItem);

                        if (baseItem instanceof StickyNote) {
                            stickyCounter++;
                        }
                    }
                }
                selectionBox.setStickyNoteSelected(stickyCounter == selectedItems.size());
                if (stickyCounter == selectedItems.size()) {
                    selectionBox.setSelectionRenderMode(1);
                } else {
                    selectionBox.setSelectionRenderMode(0);
                }


                if (selectedItems.size() == 1 && (selectedItems.get(0) instanceof Autoshape)) {
                    if (((Autoshape) selectedItems.get(0)).getType() == AutoshapeProperty.Type.ARROW || ((Autoshape) selectedItems.get(0)).getType() == AutoshapeProperty.Type.LINE) {
                        selectionBox.setHeadTailVisible(true);
                        updateHeadTailButton(selectionBox, ((Autoshape) selectedItems.get(0)).getPoints());
                    }
                }
                updateSelectionBox();
                renderSelectionPreview();
                selectionBox.setOutlineVisible(true);
                if (listener != null) {
                    listener.onSelectionBoxVisibleChange(true, selectionBox.getSelectionRenderMode());
                }

                state.setPreviewDirty(true);
                resetAutomaticPreviewUpdate();
                renderCached();
                invalidateRender();
                break;
            }
            case InputEvent.Action.MAKE_STICKY_NOTE: {
                if (selectedItems.size() == 1 && selectedItems.get(0) instanceof StickyNote) {
                    break;
                }
                int counter = 0;
                int[] deletedIndexes = new int[selectedItems.size()];
                for (int i = 0; i < state.getItems().size(); i++) {
                    if (state.getItems().get(i).isSelected()) {
                        deletedIndexes[counter++] = i;
                    }
                }

                final Bitmap bitmap = Bitmap.createBitmap((int) selectionBox.getWidth(),
                        (int) selectionBox.getHeight(), Bitmap.Config.ARGB_8888);
                runRenderStickyNoteView(bitmap);

                final StickyNote stickyNote = new StickyNote(bitmap, selectionBox.getX(),
                        selectionBox.getY());

                jsonHandler.sendSaveJsonImageToInternal(bitmap, stickyNote.getBitmapFileName());

                stickyNote.setColor(ContextCompat.getColor(context, R.color.sticky_note_pink));
                state.addItem(stickyNote);

                final long[] id = new long[]{stickyNote.getId()};
                Action action = Action.createStickyNoteAction(id, selectedItems, deletedIndexes);
                state.addAction(action);

                state.removeItems(deletedIndexes);
                clearSelectedItems();

                // following part is for set the created stickynote selected

                // set input mode to select
                onInputChange(currentDevice, Input.SELECT);

                // set stickynote selected
                stickyNote.setSelected(true);
                selectedItems.add(stickyNote);
                selectionBox.setStickyNoteSelected(true);

                // update selectionbox mode
                selectionBox.setSelectionRenderMode(SelectionBox.ButtonMode.STICKYNOTE);

                selectionBox.setHeadTailVisible(false);
                updateSelectionBox();
                renderSelectionPreview();
                selectionBox.setOutlineVisible(true);
                if (listener != null) {
                    listener.onSelectionBoxVisibleChange(true, selectionBox.getSelectionRenderMode());
                }
                selectionMode = SelectionMode.NONE;

                state.setPreviewDirty(true);
                resetAutomaticPreviewUpdate();
                renderCached();
                invalidateRender();

//                if (selectedItems.size() == 1 && selectedItems.get(0) instanceof StickyNote) {
//                    break;
//                }
//                int counter = 0;
//                int[] deletedIndexes = new int[selectedItems.size()];
//                for (int i = 0; i < state.getItems().size(); i++) {
//                    if (state.getItems().get(i).isSelected()) {
//                        deletedIndexes[counter++] = i;
//                    }
//                }
//
//                final Bitmap bitmap = Bitmap.createBitmap((int) selectionBox.getWidth(),
//                        (int) selectionBox.getHeight(), Bitmap.Config.ARGB_8888);
//                runRenderStickyNoteView(bitmap);
//
//                final StickyNote stickyNote = new StickyNote(bitmap, selectionBox.getX(),
//                        selectionBox.getY());
//                stickyNote.setColor(ContextCompat.getColor(context, R.color.sticky_note_pink));
//                state.addItem(stickyNote);
//
//                final long[] id = new long[] {stickyNote.getId()};
//                Action action = Action.createStickyNoteAction(id, selectedItems, deletedIndexes);
//                state.addAction(action);
//
//                state.removeItems(deletedIndexes);
//                clearSelectedItems();
//
//                // following part is for set the created stickynote selected
//
//                // set input mode to select
//                onInputChange(currentDevice, Input.SELECT);
//
//                // set stickynote selected
//                stickyNote.setSelected(true);
//                selectedItems.add(stickyNote);
//                selectionBox.setStickyNoteSelected(true);
//
//                // cancel if text scale visible
//                selectionBox.setTextScaleVisible(false);
//
//                // update selectionbox mode
//                selectionBox.setSelectionRenderMode(SelectionBox.ButtonMode.STICKYNOTE);
//
//                selectionBox.setHeadTailVisible(false);
//                updateSelectionBox();
//                renderSelectionPreview();
//                selectionBox.setOutlineVisible(true);
//                if (listener != null) {
//                    listener.onSelectionBoxVisibleChange(true, selectionBox.getSelectionRenderMode());
//                }
//                selectionMode = SelectionMode.NONE;
//
//                state.setPreviewDirty(true);
//                resetAutomaticPreviewUpdate();
//                renderCached();
//                invalidateRender();
                break;
            }
            case InputEvent.Action.COLOR_CHANGED: {
                @ColorInt int color = input.getColor();
                List<Integer> colorBuffer = new ArrayList<Integer>();
                List<Integer> ids = new ArrayList<Integer>();
                for (int i = 0; i < state.getItems().size(); i++) {
                    BaseItem item = state.getItems().get(i);

                    if (item instanceof StickyNote || item instanceof Image) {
                        continue;
                    }

                    if (item.isSelected()) {
                        @ColorInt int itemColor = item.getColor();
                        if (Color.red(color) != Color.red(itemColor) || Color.blue(color) != Color.blue(itemColor) || Color.green(color) != Color.green(itemColor)) {
                            colorBuffer.add(itemColor);
                            ids.add(i);
                            item.setColor(color);
                        }
                    }
                }

                if (ids.size() > 0 && ids.size() == colorBuffer.size()) {
                    int[] id = new int[ids.size()];
                    int[] colors = new int[colorBuffer.size()];
                    for (int i = 0; i < ids.size(); i++) {
                        id[i] = ids.get(i);
                        colors[i] = colorBuffer.get(i);
                    }
                    Action action = Action.createColorChangeAction(id, colors);
                    state.addAction(action);
                }

                state.setPreviewDirty(true);
                resetAutomaticPreviewUpdate();
                renderCached();
                runRenderSelectionPreview();
                invalidateRender();
                break;
            }
            case InputEvent.Action.STICKYNOTE_COLOR_CHANGED: {
                int color = input.getColor();
                int[] colorBuffer = new int[selectedItems.size()];
                int counter = 0;
                int[] ids = new int[selectedItems.size()];
                for (int i = 0; i < state.getItems().size(); i++) {
                    BaseItem item = state.getItems().get(i);

                    if (item.isSelected()) {
                        colorBuffer[counter] = state.getItems().get(i).getColor();
                        ids[counter++] = i;
                        item.setColor(color);
                    }
                }

                Action action = Action.createColorChangeAction(ids, colorBuffer);
                state.addAction(action);

                state.setPreviewDirty(true);
                resetAutomaticPreviewUpdate();
                renderCached();
                runRenderSelectionPreview();
                invalidateRender();
                break;
            }
            case InputEvent.Action.TOUCH: {
                final MotionEvent event = input.getEvent();
                processMotionEvent(event);
                event.recycle();
                break;
            }
            case InputEvent.Action.UNDO: {
                undo();

                state.setPreviewDirty(true);
                resetAutomaticPreviewUpdate();
                invalidateRender();
                break;
            }
            case InputEvent.Action.SET_WHITEBOARD_STATE: {
                setState(input.getState());

                if (inputManager != null) {
                    Input stylusInput = inputManager.getInput(Device.STYLUS);
                    if (stylusInput == Input.TEXT || stylusInput == Input.STICKYNOTE) {
                        Input result = ((previousStylusInput != Input.TEXT && previousStylusInput != Input.STICKYNOTE) ? previousStylusInput : Input.PEN);
                        onInputChange(Device.STYLUS, result);
                        if (listener != null) {
                            listener.onCurrentInputChange(Device.STYLUS, result);
                        }
                    }
                    if (stylusInput == Input.OBJECT_ERASER || stylusInput == Input.PARTIAL_ERASER && inputManager.getInput(Device.PALM) != stylusInput) {
                        stylusInput = inputManager.getInput(Device.PALM);
                        onInputChange(Device.STYLUS, stylusInput);
                        if (listener != null) {
                            listener.onCurrentInputChange(Device.STYLUS, stylusInput);
                        }
                    }

                    // update eraser for change app mode 2020/11/02 Cosam
                    Input eraserInput = inputManager.getInput(Device.PALM);
                    onInputChange(Device.PALM, eraserInput);
                }
                if (listener != null) {
                    listener.updateStateJsonFile();
                    listener.loadRulerRecord();
                }

                invalidateRender();
                clearWhiteBoard = false;
                break;
            }
            case InputEvent.Action.ADD_IMAGE: {
                selectionBox.setHeadTailVisible(false);
                clearTextBoxAndToggleKeyboard();

                if (input.getUri() != null) {
                    addImage(input.getUri());
                } else if (input.getResultUrl() != null) {
                    addImage(input.getResultUrl(), input.getX(), input.getY());
                } else {
                    addImage(input.getImage());
                }

                state.setPreviewDirty(true);
                resetAutomaticPreviewUpdate();
                invalidateRender();
                break;
            }
            case InputEvent.Action.ACTIVE_WORD_TEXT_CHANGED: {
                if (activeWord != null) {
                    activeWord.setText(input.getText(), input.getX(), input.getY(), input.getW(), input.getH(), input.getCursor());

                    undoByEditText(input.getText(), input.getKeyCode());

                    state.setPreviewDirty(true);
                    resetAutomaticPreviewUpdate();
                    invalidateRender();
                } else {
                    log("Error : activeWord is missing , text : [" + input.getText() + "]");
                }
                break;
            }
            case InputEvent.Action.UPDATE_STATE_PREVIEW: {
                log("processInput: Action.UPDATE_STATE_PREVIEW");
                Input bottomBarState = inputManager.getInput(currentDevice);
                if (bottomBarState == Input.TEXT && activeWord != null && activeWord.getTextStatus().equals(Word.TextStatus.FOCUSED)) {
                    clearTextBoxAndToggleKeyboard();
                    state.setPreviewDirty(true);
                    resetAutomaticPreviewUpdate();
                }
                log("- state.isPreviewDirty=" + state.isPreviewDirty());
                if (state.isPreviewDirty()) {
                    runRenderStatePreview(state);
                    invalidateRender();

                    if (listener != null && state.getItems().size() > 0) {
                        listener.updateStateJsonFile();
                    }
                }
                break;
            }
            case InputEvent.Action.CLEAR_BOARD: {
                clearItems();

                keyFrameBufferLockOffAppWriteActionMoveCache.clear();

                state.setPreviewDirty(true);
                resetAutomaticPreviewUpdate();
                if (Props.getMainboardVersion() != Props.Mainboard.V_811) {
                    cleanFrameBufferCount = 0;
                    invalidateRender();
                } else {
                    invalidateRender();
                }
                break;
            }
            case InputEvent.Action.TRIMING: {
                selectionBox.setTrimRectF(selectionBox.getLeft() + 10, selectionBox.getTop() + 10, selectionBox.getRight() - 10, selectionBox.getBottom() - 10);
                isTrimmingActive = !isTrimmingActive;
                selectionMode = SelectionMode.TRIMMING;
                selectionBox.setImageTrimming(isTrimmingActive);

                state.setPreviewDirty(true);
                resetAutomaticPreviewUpdate();
                invalidateRender();
                break;
            }
            case InputEvent.Action.PUSH_END: {
                pushEnd();

                state.setPreviewDirty(true);
                resetAutomaticPreviewUpdate();
                invalidateRender();
                break;
            }
            case InputEvent.Action.INPUT_CHANGED: {
                onInputChange(input.getDevice(), input.getInput());
                break;
            }
            case InputEvent.Action.ON_STOP: {
                resetTouch();
                invalidateRender();
                break;
            }
            case InputEvent.Action.GET_READY_FOR_SELECTION: {
                switch (input.getText()) {
                    case CLEAR_TEXT_SELECTION_AREA_ACTION: {
                        setTextSelectionAreaVisible(false);
                        renderCached();
                        invalidateRender();
                        break;
                    }
                    case RESET_TEXT_SELECTION_AREA_ACTION: {
                        Input bottomBarState = inputManager.getInput(currentDevice);
                        setTextSelectionAreaVisible(bottomBarState == Input.TEXT);
                        renderCached();
                        invalidateRender();
                        break;
                    }
                }
                break;
            }
            case InputEvent.Action.INVALIDATE_RENDER: {
                invalidateRender();
                break;
            }
            case InputEvent.Action.REMOVE_ACTIVE_ITEMS: {

                for (int i = 0; i < MAX_INPUT_POINTERS; i++) {
                    if (pointerInputs[i] != null) {
                        onTouchUp(i, lastDragPoints[i].x, lastDragPoints[i].y);
                    }
                }

                this.keyFrameBufferLockOffAppWriteActionMoveCache.clear();//两指事件时clear

                removeActiveItems(input.getBooleanValue());
                break;
            }
            case InputEvent.Action.CLEAN_FRAME_BUFFER: {
                cleanFrameBuffer();
                break;
            }
            case InputEvent.Action.REDO: {
                redo();

                state.setPreviewDirty(true);
                resetAutomaticPreviewUpdate();
                invalidateRender();
                break;
            }
            case InputEvent.Action.SET_SIZE: {
                this.width = (int) input.getW();
                this.height = (int) input.getH();

                updateByDpi(width, height);

                // set bitmap for render cache
                final BitmapPool pool = GlideApp.get(context).getBitmapPool();
                if (cached != null) {
                    pool.put(cached);
                }
                if (eraserScratchCached != null) {
                    pool.put(eraserScratchCached);
                }

                if (eraserBackground != null) {
                    pool.put(eraserBackground);
                }

                cached = pool.getDirty(width, height, Bitmap.Config.ARGB_8888);
                eraserScratchCached = pool.getDirty(width, height, Bitmap.Config.ARGB_8888);
                eraserBackground = pool.getDirty(width, height, Bitmap.Config.ARGB_8888);
                if (FastDraw.isSupportedCanvasFastDrawEraser()) {
                    Bitmap fastDrawEraserBitmap = fastDraw.getFastDrawEraserBitmap();
                    if (fastDrawEraserBitmap != null) {
                        pool.put(fastDrawEraserBitmap);
                    }
                    fastDrawEraserBitmap = pool.getDirty(width, height, Bitmap.Config.ARGB_8888);
                    fastDraw.setFastDrawEraserBitmap(fastDrawEraserBitmap);
                }

                // get drawable for page pattern
                if (renderPatternDrawable == null && state != null && state.hasPagePattern()) {
                    if (state.isPageCustomBackgroundSet()) {
                        renderPatternDrawable = state.getPageCustomBackground();
                        setCenteredBounds(renderPatternDrawable);
                    } else {
                        if (state.getPagePatternRes() == -1) {
                            // fail load
                            renderPatternDrawable = ContextCompat.getDrawable(context, PageProperty.Pattern.BLANK.ordinal());
                        } else {
                            renderPatternDrawable = ContextCompat.getDrawable(context, state.getPagePatternRes());
                        }
                    }
                    renderPatternRes = state.getPagePatternRes();
                }

                if (renderPatternDrawable != null && !state.isPageCustomBackgroundSet()) {
                    renderPatternDrawable.setBounds(0, 0, width, height);
                }

                renderCached();
                invalidateRender();
                break;
            }
            case InputEvent.Action.PROTRACTOR_DRAWING: {
                Device device = getDevice(0);
                if (device == null || input.getPoints() == null || input.getPoints().size() < 2) {
                    break;
                }

                float degree = input.getX();
                PointF centerP = input.getPoints().get(0);
                PointF startP = input.getPoints().get(1);
                ProtractorPaintMode mode = ProtractorPaintMode.values()[input.getKeyCode()];
                Input i = inputManager.getInput(device);
                LineProperty lineProperty = getLineProperty(device, i);
                if (lineProperty == null) {
                    break;
                }
                if (protractorItem == null) {
                    protractorItem = new ProtractorItem(baseContext, lineProperty, state.getAppliedScale(), centerP, startP, degree, mode);
                }
                protractorItem.setCenterP(centerP.x, centerP.y);
                protractorItem.setStartP(startP.x, startP.y);
                protractorItem.setProtractorDegree(degree);

                invalidateRender();

                break;
            }
            case InputEvent.Action.PROTRACTOR_FINISH: {
                Device device = getDevice(0);
                if (device == null || input.getPoints() == null || input.getPoints().size() < 2) {
                    break;
                }

                float degree = input.getX();
                PointF centerP = input.getPoints().get(0);
                PointF startP = input.getPoints().get(1);

                if (protractorItem != null) {
                    protractorItem.setCenterP(centerP.x, centerP.y);
                    protractorItem.setStartP(startP.x, startP.y);
                    protractorItem.setProtractorDegree(degree);
                    if (protractorItem.getProtractorDegree() != 0) {
                        ProtractorPaintMode mode = ProtractorPaintMode.values()[input.getKeyCode()];
                        if (mode == ProtractorPaintMode.ARC) {
                            protractorItem.setShowDegree(false);
                        }
                        cacheQueue.add(protractorItem);
                        state.addItem(protractorItem);
                        Action action = Action.createInsertAction(protractorItem.getId());
                        state.addAction(action);
                    }

                    protractorItem = null;

                    renderCached();
                    invalidateRender();
                    state.setPreviewDirty(true);
                }

                break;
            }
            case InputEvent.Action.RULER_DRAWING: {
                Device device = getDevice(0);
                if (device == null || input.getPoints() == null || input.getPoints().size() <= 0) {
                    break;
                }
                Input i = inputManager.getInput(device);

                if (rulerHelper == null) {
                    LineProperty lineProperty = getLineProperty(device, i);
                    if (lineProperty == null) {
                        break;
                    }
                    rulerHelper = new RulerHelper(input.getPoints(), lineProperty, state.getAppliedScale());
                } else {
                    rulerHelper.setPoints(input.getPoints());
                }

                invalidateRender();
                break;
            }
            case InputEvent.Action.RULER_FINISH: {
                if (rulerHelper != null && input.getPoints() != null && input.getPoints().size() > 0) {
                    rulerHelper.setPoints(input.getPoints());
                    Line line = rulerHelper.createLine(context, state.getAppliedScale());
                    rulerHelper = null;
                    if (line == null) {
                        break;
                    }
                    cacheQueue.add(line);
                    state.addItem(line);
                    Action action = Action.createInsertAction(line.getId());
                    state.addAction(action);
                    renderCached();
                    invalidateRender();
                    state.setPreviewDirty(true);
                }
                break;
            }
        }
    }

    private LineProperty getLineProperty(Device device, Input i) {
        LineProperty lineProperty = null;
        if (device == Device.STYLUS) {
            if (i == Input.PEN) {
                lineProperty = stylusPenProperty;
            } else if (i == Input.HIGHLIGHTER) {
                lineProperty = stylusHighlighterProperty;
            }
        } else if (device == Device.FINGER) {
            if (i == Input.PEN) {
                lineProperty = fingerPenProperty;
            } else if (i == Input.HIGHLIGHTER) {
                lineProperty = fingerHighlighterProperty;
            }
        }

        return lineProperty;
    }

    /**
     * 批注模式下不锁缓存，先保存应用层要画的点（加速层写完（ACTION_UP）后再在Canvas上画出应用层的绘画）
     *
     * @param pointerId
     * @param pointerX
     * @param pointerY
     * @param event
     * @return
     */
    private boolean annotationStateRecord(int pointerId, float pointerX, float pointerY, MotionEvent event) {
        Input input = getInputForPointer(pointerId);
        if (!fastDraw.isSupportedCanvasFastDraw() || !state.isAnnotationState() || input == Input.AUTOSHAPE || input == Input.OBJECT_ERASER || input == Input.SELECT || input == Input.PARTIAL_ERASER) {
            keyFrameBufferLockOffAppWriteActionMoveCache.clear();
            return false;
        }
        Map map = new HashMap<>();
        map.put("pointerId", pointerId);
        map.put("pointerX", pointerX);
        map.put("pointerY", pointerY);
        keyFrameBufferLockOffAppWriteActionMoveCache.add(map);
        return true;
    }

    /**
     * 批注模式下不锁缓存，加速层写完（ACTION_UP）后再在Canvas上画出应用层的绘画
     */
    private void annotationStateDrag(int pointerId) {
        if (fastDraw.isSupportedCanvasFastDraw() && state.isAnnotationState()) {
            for (Map tmpAppWriteCache : keyFrameBufferLockOffAppWriteActionMoveCache) {
                final int tmpPointerId = (int) tmpAppWriteCache.get("pointerId");
                final float tmpPointerX = (float) tmpAppWriteCache.get("pointerX");
                final float tmpPointerY = (float) tmpAppWriteCache.get("pointerY");
                onTouchDrag(0, tmpPointerId, tmpPointerX, tmpPointerY, 0);
            }
        }
        this.keyFrameBufferLockOffAppWriteActionMoveCache.clear();
    }

    private void processMotionEvent(MotionEvent event) {
        resetTouchEventTimer();

        gestureDetector.onTouchEvent(event);

        tempPointF.set(event.getX(), event.getY());
        scrollTransform(tempPointF);
        final float x = tempPointF.x;
        final float y = tempPointF.y;

        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN: {
                log("processMotionEvent: MotionEvent.ACTION_DOWN");
                final int pointerId = 0;
                //橡皮擦down时，隐藏悬浮窗
                if (getInputForPointer(pointerId) == Input.OBJECT_ERASER || getInputForPointer(pointerId) == Input.PARTIAL_ERASER)
                    BroadcastHelper.sendScreenshotStart(context);
                Device device = TouchUtils.getEventDevice(context, pointerId, event);
                log("processmotionevent device:" + device);
                if (getInputForPointer(pointerId) == Input.MOVE_CANVAS && device == Device.PALM && !FastDraw.isSupportedCanvasFastDrawEraser()) {
                    device = Device.FINGER;
                }
                if (getDevice(pointerId) != null) {
                    if (dragPoints[pointerId] != null) {
                        if (!dragPoints[pointerId].equals(0, 0)) {
                            log("in ACTION_DOWN: send touch up.");
                            if (!annotationStateRecord(pointerId, dragPoints[pointerId].x, dragPoints[pointerId].y, null))
                                onTouchUp(pointerId, dragPoints[pointerId].x, dragPoints[pointerId].y);
                            fingerActiveCount = event.getPointerCount();
                        }
                    }
                }
                setDevice(pointerId, device);

                onTouchDown(0, pointerId, x, y, event);
                break;
            }
            case MotionEvent.ACTION_POINTER_DOWN: {
//                log("processMotionEvent: MotionEvent.ACTION_POINTER_DOWN");
                final int index = event.getActionIndex();
                final int pointerId = event.getPointerId(index);
                //橡皮擦down时，隐藏悬浮窗
                if (getInputForPointer(pointerId) == Input.OBJECT_ERASER || getInputForPointer(pointerId) == Input.PARTIAL_ERASER)
                    BroadcastHelper.sendScreenshotStart(context);
                tempPointF.set(event.getX(index), event.getY(index));
                scrollTransform(tempPointF);
                final float pointerX = tempPointF.x;
                final float pointerY = tempPointF.y;

                log("- pointerId=" + pointerId);
                if (pointerId < MAX_INPUT_POINTERS) {
                    Device device = TouchUtils.getEventDevice(context, pointerId, event);
                    if (getInputForPointer(pointerId) == Input.MOVE_CANVAS && device == Device.PALM) {
                        device = Device.FINGER;
                    }
                    if (getDevice(pointerId) != null) {
                        if (dragPoints[pointerId] != null && !dragPoints[pointerId].equals(0, 0)) {
                            log("send onTouchUp to the reuse of same pointer. finger count:" + event.getPointerCount());
                            fingerActiveCount = event.getPointerCount();

                            if (!annotationStateRecord(pointerId, dragPoints[pointerId].x, dragPoints[pointerId].y, null)) {
                                onTouchUp(pointerId, dragPoints[pointerId].x, dragPoints[pointerId].y);
                            }
                        }
                    }
                    setDevice(pointerId, device);

                    onTouchDown(index, pointerId, pointerX, pointerY, event);
                }
                break;
            }
            case MotionEvent.ACTION_MOVE: {
//                Log.d(TAG, "processMotionEvent: ACTION_MOVE");
                final int pointerCount = event.getPointerCount();

                final int historySize = event.getHistorySize();
                for (int h = 0; h < historySize; h++) {
                    for (int i = 0; i < pointerCount; i++) {
                        final int pointerId = event.getPointerId(i);

                        tempPointF.set(event.getHistoricalX(i, h), event.getHistoricalY(i, h));
                        scrollTransform(tempPointF);
                        final float pointerX = tempPointF.x;
                        final float pointerY = tempPointF.y;

                        if (pointerId < MAX_INPUT_POINTERS) {
                            PointF last = lastEvents[pointerId];
                            if (last == null) {
                                lastEvents[pointerId] = new PointF(pointerX, pointerY);
                                if (!annotationStateRecord(pointerId, pointerX, pointerY, null))
                                    onTouchDrag(i, pointerId, pointerX, pointerY, event.getEventTime());
                            } else if (last.x != pointerX || last.y != pointerY) {
                                last.set(pointerX, pointerY);
                                if (!annotationStateRecord(pointerId, pointerX, pointerY, null))
                                    onTouchDrag(i, pointerId, pointerX, pointerY, event.getEventTime());
                            }
                        }
                    }
                }

                for (int i = 0; i < pointerCount; i++) {
                    final int pointerId = event.getPointerId(i);

                    tempPointF.set(event.getX(i), event.getY(i));
                    scrollTransform(tempPointF);
                    final float pointerX = tempPointF.x;
                    final float pointerY = tempPointF.y;

                    if (pointerId < MAX_INPUT_POINTERS) {
                        PointF last = lastEvents[pointerId];
                        if (last == null) {
                            lastEvents[pointerId] = new PointF(pointerX, pointerY);
                            if (!annotationStateRecord(pointerId, pointerX, pointerY, null))
                                onTouchDrag(i, pointerId, pointerX, pointerY, event.getEventTime());
                        } else if (last.x != pointerX || last.y != pointerY) {
                            last.set(pointerX, pointerY);
                            if (!annotationStateRecord(pointerId, pointerX, pointerY, null))
                                onTouchDrag(i, pointerId, pointerX, pointerY, event.getEventTime());
                        }
                    }
                }
                break;
            }
            case MotionEvent.ACTION_POINTER_UP: {
                final int index = event.getActionIndex();
                final int pointerId = event.getPointerId(index);
                //橡皮擦UP时，显示悬浮窗
                if (getInputForPointer(pointerId) == Input.OBJECT_ERASER || getInputForPointer(pointerId) == Input.PARTIAL_ERASER)
                    BroadcastHelper.sendScreenshotEnd(context);
                tempPointF.set(event.getX(index), event.getY(index));
                scrollTransform(tempPointF);
                final float pointerX = tempPointF.x;
                final float pointerY = tempPointF.y;
                fingerActiveCount = event.getPointerCount();

                if (pointerId < MAX_INPUT_POINTERS) {
                    annotationStateDrag(pointerId);
                    onTouchUp(pointerId, pointerX, pointerY);
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                final int pointerId = event.getPointerId(event.getActionIndex());
                //橡皮擦UP时，显示悬浮窗
                if (getInputForPointer(pointerId) == Input.OBJECT_ERASER || getInputForPointer(pointerId) == Input.PARTIAL_ERASER)
                    BroadcastHelper.sendScreenshotEnd(context);
                fingerActiveCount = event.getPointerCount();

                if (pointerId < MAX_INPUT_POINTERS) {
                    annotationStateDrag(pointerId);
                    onTouchUp(pointerId, x, y);
                }
                break;
            }
            case MotionEvent.ACTION_CANCEL: {
                jobsBeforeSuspend();
                break;
            }
        }
    }

    @Override
    public void onInvalidatorCreated(RenderThread.Invalidator invalidator) {
        this.invalidator = invalidator;
    }

    private void invalidateRender() {
        invalidator.invalidate();
    }

    private void cleanFrameBuffer() {
        log("cleanFrameBuffer");
        if (FastDraw.isSupported()) {
            if (Props.getMainboardVersion() != Props.Mainboard.V_811
                    ||Props.getFlavorChipset().equals(Props.Flavor.Chipset.Optoma)
                    ||Props.getFlavorChipset().equals(Props.Flavor.Chipset.Amlogic)) {
                cleanFrameBufferCount = 0;
                invalidateRender();
            } else {
                invalidateRender();
            }
        }
    }

    private void removeActiveItems(boolean cleanFrameBuffer) {
        log("removeActionItems");
        activeItems.clear();

        for (int i = 0; i < MAX_INPUT_POINTERS; i++) {
            pointerInputs[i] = null;
            selectionControl = SelectionBox.Control.NONE;

            downPoints[i].set(0, 0);
            dragPoints[i].set(0, 0);
        }

        renderCached();
        invalidateRender();

        if (cleanFrameBuffer) {
            cleanFrameBuffer();
        }
    }

    private void cancelFling() {
        log("cancelFling");
        isFlinging = false;

        // scroll grid
        scrollGridStartX += scrollX;
        scrollGridStartY += scrollY;

//        // background
//        backgroundX += (int) scrollX;
//        backgroundY += (int) scrollY;

        // reset scrolling
        transform.clear();
        transform.setTranslate(scrollX, scrollY);
        transform.apply(state.getItems());
        transform.clear();
        scrollX = 0;
        scrollY = 0;
    }

    @Override
    public boolean process() {
        final InputEvent event = bufferQueue.poll();
        if (event != null) {
            processInput(event);
            return true;
        } else {
            if (!bufferQueue.isEmpty()) {// do not use size() to access size of concurrentLinkedDeque, the execution time is not a constant.
                SharedPreferences prefs = context.getSharedPreferences(sharePreference, Context.MODE_PRIVATE);
                prefs.edit().putString("THREAD_SAFE", "Error : thread safe concurrency bug").apply();
            }

            return false;
        }
    }

    @Override
    public void preRender() {
        if (doRenderCached) {
            if (state != null) {
                runRenderCached();
            }
            doRenderCached = false;
        }
        if (doRenderSelectionPreview) {
            if (state != null) {
                runRenderSelectionPreview();
            }
            doRenderSelectionPreview = false;
        }
    }

    @Override
    public void postRender() {
        if (state.isPreviewDirty()
                && System.currentTimeMillis() - lastStateChangeTime >= TIMEOUT_RENDER_PREVIEW) {
            runRenderStatePreview(state);
            if (listener != null && state.getItems().size() > 0) {
                listener.updateStateJsonFile();
            }
        }

        final long now = System.currentTimeMillis();

        if (now - lastTouchEventTime >= TIMEOUT_TOUCH_EVENT && fingerActiveCount != 0) {
            resetTouch();
            invalidateRender();
        }

        if (isFlinging && scrollPointerId == INVALID_POINTER_ID) {
//            long now = System.currentTimeMillis();
            float dx = flingHelper.getX(now - scrollFlingStart);
            float dy = flingHelper.getY(now - scrollFlingStart);

            if (Math.abs(dx) <= Math.abs(scrollX) && Math.abs(dy) <= Math.abs(scrollY)) {
                cancelFling();
                renderCached();
            } else {
                scrollX = dx;
                scrollY = dy;

                scrollbgXs[MainActivity.stateIndex] += dx;
                scrollbgYs[MainActivity.stateIndex] += dy;
            }

            log("- isFlinging=" + isFlinging + ", scrollPointerId=" + scrollPointerId);
            invalidateRender();
        }
        if (FastDraw.isSupportedCanvasFastDraw()) {
            fastDraw.consumePaths();
        }

        if (notifyEraserDone) {
            notifyEraserDone = false;
            listener.onEraserRenderOver();
        }
    }

    @Override
    public void render(Canvas canvas) {

        if (state == null) {
            return;
        }
//        log("render");
        if (clearWhiteBoard) {
            canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
            return;
        }

        renderBackground(canvas);

        if (partialEraserPen != null) {
            renderPartialErasedCached();
        }

        canvas.save();
        if (scrollX != 0 || scrollY != 0) {
            canvas.translate(scrollX, scrollY);
        }

        canvas.save();
//        log("- zoomScale=" + zoomScale);
        if (zoomScale != 0) {
            canvas.scale(zoomScale, zoomScale, zoomPoint.x, zoomPoint.y);

        }


        // draw cached
        if ((isMultiFingerActive || (isFlinging && scrollPointerId == INVALID_POINTER_ID))) {
            drawScrollGridByYuda(canvas);
            for (BaseItem item : state.getItems()) {
                item.render(canvas, paint, context);
            }
        } else {
            if (cached != null && !cached.isRecycled()) {
//                log("- draw cached");
                paint.setAlpha(0xff);
                canvas.drawBitmap(cached, 0, 0, paint);
            } else if (cached != null) {
                renderCached();
                invalidateRender();
            }
        }

        if (!fastDraw.isDrawing()) {
            // draw active items
            for (int i = 0, size = activeItems.size(); i < size; i++) {
                final BaseItem item = activeItems.get(i);
                log("activeItems.0.null?:" + (activeItems.get(i) == null));
                if (item != null) {
//            if (item != null && !(item instanceof Word)) {
                    //                log("draw active items.");
                    item.render(canvas, paint, context);
                    if (item instanceof Line && fastDraw != null) {
                        Line line = (Line) item;
                        if (FastDraw.isSupportedCanvasFastDraw()) {
                            fastDraw.setSnakePathPointsLimit(i, System.currentTimeMillis(), line.getPoints(), line.getColor(), line.getThickness());
                        }
                    }
                }
            }
        }

        // draw trimming
        if (isTrimmingActive) {
            int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
            canvas.drawRect(selectionBox.getLeft(), selectionBox.getTop(), selectionBox.getRight(), selectionBox.getBottom(), coverPaint);
            canvas.drawRect(selectionBox.getTrimRectF(), transparentPaint);
            canvas.restoreToCount(layerId);
        }

        // draw scaling text box preview
        Word activeWord = null;
        if (selectedItems.size() == 1 && selectedItems.get(0) instanceof Word) {
            activeWord = (Word) selectedItems.get(0);
        }
        if (activeWord != null && selectionMode.equals(SelectionMode.TEXT_SCALE)) {
            activeWord.render(canvas, paint, context);
        }

        // draw selection box
        if (selectionLine.isVisible()) {
            selectionLine.render(canvas, paint, context);
            if (selectionLine.isAnimating()) {
                invalidateRender(); // request invalidate as long as there is a selection line to be drawn
            }
        }
        if (selectionBox.isVisible()) {
            selectionBox.render(canvas, paint, context);
        }

        if (rulerHelper != null) {
            rulerHelper.render(canvas);
        }

        if (protractorItem != null) {
            protractorItem.render(canvas, paint, context);
        }

        if (objectEraserPen != null) {
            objectEraserPen.render(canvas, null, context);
        }

        canvas.restore(); // undo scale

        canvas.restore(); // undo translation

        // draw add text hint
        if (showTapScreen) {
            paint.setTextSize(hintTextSize);
            paint.setColor(isLastPageColorDark ? hintAddTextColorLight : hintAddTextColorDark);
            paint.setAlpha(getBlinkAlpha(HINT_ADD_TEXT_ALPHA));
            paint.setStyle(Paint.Style.FILL);
            canvas.drawText(hintAddText, (width - hintAddTextLength) / 2.0f, height / 2.0f, paint);
            invalidateRender();
        }

        if (showHintText) {
            paint.setTextSize(hintTextSize);
            paint.setColor(isLastPageColorDark ? hintTextColorDark : hintTextColorLight);
            paint.setStyle(Paint.Style.FILL);
            canvas.drawText(hintText, (width - hintTextLength) / 2.0f, height / 2.0f, paint);
        }

        // clean frame buffer
        if (cleanFrameBufferCount < TOTAL_CLEAN_FRAME_BUFFER_COUNT) {
            if (state.isAnnotationState()) {
                canvas.drawColor(cleanFrameBufferCount % 2 == 0 ? 0x04aaaaaa : 0x04666666);
            } else {
                canvas.drawColor(isLastPageColorDark ? CLEAN_FRAME_BUFFER_COLOR_DARK : CLEAN_FRAME_BUFFER_COLOR_LIGHT);
            }
            cleanFrameBufferCount++;
            invalidateRender();
        }
    }

    private void renderBackground(Canvas canvas) {
        // update saved page color
        final int pageColorRes = state.getPageColorRes();
        if (lastPageColorRes != pageColorRes) {
            lastPageColorRes = pageColorRes;
            lastPageColor = ContextCompat.getColor(context, pageColorRes);
            isLastPageColorDark = state.isPageColorDark();
        }

        // clear canvas with page color
        canvas.drawColor(lastPageColor, PorterDuff.Mode.SRC);

        // get page pattern drawable if changed
        if (state.isPageCustomBackgroundSet()) {
            if (renderPatternDrawable != state.getPageCustomBackground()) {
                renderPatternDrawable = state.getPageCustomBackground();
                renderPatternRes = state.getPagePatternRes();
            }
        } else {
            final int pagePatternRes = state.getPagePatternRes();
            if (pagePatternRes != renderPatternRes) {
                if (pagePatternRes != 0) {
                    renderPatternDrawable = ContextCompat.getDrawable(context, pagePatternRes);
                } else {
                    renderPatternDrawable = null;
                }
                renderPatternRes = pagePatternRes;
            }
        }

        if (renderPatternDrawable != null) {
            setBackgroundBounds(renderPatternDrawable);
            renderPatternDrawable.draw(canvas);
        }
        listener.renderBackgroundFinished();
    }


    private void drawScrollGrid(Canvas canvas) {
        // horizontal lines

        int lines = (int) (height / scrollGridSpacing) + 1;
        float start = -(scrollGridStartY + scrollY) + (scrollGridStartY + scrollY) % scrollGridSpacing;
        float[] points = new float[4 * lines];

        points[0] = -(scrollGridStartX + scrollX);
        points[1] = start;
        points[2] = points[0] + width;
        points[3] = points[1];

        for (int i = 4; i < 4 * lines; i += 4) {
            points[i] = points[i - 4];
            points[i + 1] = points[i - 3] + scrollGridSpacing;
            points[i + 2] = points[i - 2];
            points[i + 3] = points[i - 1] + scrollGridSpacing;
        }

        paint.setColor(isLastPageColorDark ? scrollGridColorDark : scrollGridColorLight);
        paint.setStrokeWidth(1.0f);
        canvas.drawLines(points, 0, points.length, paint);

        // vertical lines

        lines = (int) (width / scrollGridSpacing) + 1;
        start = -(scrollGridStartX + scrollX) + (scrollGridStartX + scrollX) % scrollGridSpacing;
        points = new float[4 * lines];

        points[0] = start;
        points[1] = -(scrollGridStartY + scrollY);
        points[2] = points[0];
        points[3] = points[1] + height;

        for (int i = 4; i < 4 * lines; i += 4) {
            points[i] = points[i - 4] + scrollGridSpacing;
            points[i + 1] = points[i - 3];
            points[i + 2] = points[i - 2] + scrollGridSpacing;
            points[i + 3] = points[i - 1];
        }

        canvas.drawLines(points, 0, points.length, paint);
    }

    private void drawScrollGridByYuda(Canvas canvas) {
        float gridScaleRatio = 1 / zoomScale;

        float scrollGridOffsetX = (scrollGridSpacing - scrollX % scrollGridSpacing);
        float scrollGridOffsetY = (scrollGridSpacing - scrollY % scrollGridSpacing);
        float scaleX = (int) ((zoomPoint.x + scrollX + (0 - (zoomPoint.x + scrollX)) * gridScaleRatio) / scrollGridSpacing) * scrollGridSpacing;
        float scaleY = (int) ((zoomPoint.y + scrollY + (0 - (zoomPoint.y + scrollY)) * gridScaleRatio) / scrollGridSpacing) * scrollGridSpacing;
        float gridX = -scrollX - scrollGridOffsetX + scaleX;
        float gridY = -scrollY - scrollGridOffsetY + scaleY;

        // TODO : calculate the correct ratio.
        float scaleRatio = Math.max(gridScaleRatio, 1);

        // horizontal lines

        int lines = (int) ((height + scrollGridOffsetY + scrollGridSpacing) * scaleRatio / scrollGridSpacing) + 1;
        float[] points = new float[4 * lines];

        points[0] = gridX;
        points[1] = gridY;
        points[2] = points[0] + (width + scrollGridOffsetX + scrollGridSpacing) * scaleRatio;
        points[3] = points[1];

        for (int i = 4; i < 4 * lines; i += 4) {
            points[i] = points[i - 4];
            points[i + 1] = points[i - 3] + scrollGridSpacing;
            points[i + 2] = points[i - 2];
            points[i + 3] = points[i - 1] + scrollGridSpacing;
        }

        paint.setColor(isLastPageColorDark ? scrollGridColorDark : scrollGridColorLight);
        paint.setStrokeWidth(1.0f);
        canvas.drawLines(points, 0, points.length, paint);

        // vertical lines

        lines = (int) ((width + scrollGridOffsetX + scrollGridSpacing) * scaleRatio / scrollGridSpacing) + 1;
        points = new float[4 * lines];

        points[0] = gridX;
        points[1] = gridY;
        points[2] = points[0];
        points[3] = points[1] + (height + scrollGridOffsetY + scrollGridSpacing) * scaleRatio;

        for (int i = 4; i < 4 * lines; i += 4) {
            points[i] = points[i - 4] + scrollGridSpacing;
            points[i + 1] = points[i - 3];
            points[i + 2] = points[i - 2] + scrollGridSpacing;
            points[i + 3] = points[i - 1];
        }

        canvas.drawLines(points, 0, points.length, paint);
    }

    private void setBackgroundBounds(Drawable background) {
        setCenteredBounds(background);
    }

    /**
     * Renders inactive items onto the cached canvas.
     */
    private void renderCached() {
        doRenderCached = true;
    }

    /**
     * Do not call this method directly.  Use {@link #renderCached()} instead.
     */
    private void runRenderCached() {
        log("runRenderCached");
        offscreenCanvas.setBitmap(cached);

        if (cacheQueue.isEmpty()) {
            offscreenCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);

            for (BaseItem item : state.getItems()) {
                if (!(item instanceof Word)) {
                    item.render(offscreenCanvas, paint, context);
                } else {
                    Word.TextStatus status = ((Word) item).getTextStatus();
                    if (!status.equals(Word.TextStatus.FOCUSED)) {
                        item.render(offscreenCanvas, paint, context);
                    }
                }
            }
        } else {
            BaseItem item;
            while ((item = cacheQueue.poll()) != null) {
                if (!(item instanceof Word)) {
                    item.render(offscreenCanvas, paint, context);
                    if (item instanceof Line) {
                        Line line = (Line) item;
                        log("ColorTranslucent:" + Color.alpha(line.getColor()));
                        if (FastDraw.isSupportedCanvasFastDraw()) {
                            fastDraw.addConsumePath(line.getPath(), line.getThickness(), line.getColor());
                        }
                    }
                } else {
                    Word.TextStatus status = ((Word) item).getTextStatus();
                    if (!status.equals(Word.TextStatus.FOCUSED)) {
                        item.render(offscreenCanvas, paint, context);
                    }
                }
            }
        }
    }

    private void renderPartialErasedCached() {
        offscreenCanvas.setBitmap(cached);
        offscreenCanvas.save();
        partialEraserPen.render(offscreenCanvas, partialEraserPen.getEraserPaint(), context);
        offscreenCanvas.restore();
    }

    public Bitmap getEraserScratchCached() {
        Canvas canvas = new Canvas();
        canvas.setBitmap(eraserScratchCached);
        canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
        for (BaseItem item : state.getItems()) {
            if (!(item instanceof Line)) {
                item.render(canvas, paint, context);
            }
        }
        return eraserScratchCached;
    }

    public Bitmap getFrontSurfaceEraserScratchCached() {
        Canvas canvas = new Canvas();
        canvas.setBitmap(eraserScratchCached);
        renderBackground(canvas);
        for (BaseItem item : state.getItems()) {
            if (!(item instanceof Line)) {
                item.render(canvas, paint, context);
            }
        }
        return eraserScratchCached;
    }

    public Bitmap getEraserBackgroundCached() {
        Canvas canvas = new Canvas();
        canvas.setBitmap(eraserBackground);
        renderBackground(canvas);
        for (BaseItem item : state.getItems()) {
            item.render(canvas, paint, context);
        }
        return eraserBackground;
    }

    public void renderFastDrawEraserCached() {
        if (fastDraw.getFastDrawEraserBitmap() != null && !fastDraw.getFastDrawEraserBitmap().isRecycled()
                && fastDrawEraserCanvas != null) {
            fastDrawEraserCanvas.setBitmap(fastDraw.getFastDrawEraserBitmap());
            renderBackground(fastDrawEraserCanvas);

            fastDrawEraserCanvas.save();
            for (BaseItem item : state.getItems()) {
                if (!(item instanceof Line)) {
                    item.render(fastDrawEraserCanvas, paint, context);
                }
            }

            Paint paint = new Paint();
            paint.setColor(Color.TRANSPARENT);
            paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));

            // update regions which should be ignored by eraser
            if (listener != null) {
                listener.reDrawViewOnCanvas(fastDrawEraserCanvas, width, height, paint);
            }

            fastDrawEraserCanvas.restore();
        }
    }

    /**
     * Renders selected items onto the offscreen canvas.
     */
    private void renderSelectionPreview() {
        doRenderSelectionPreview = true;
    }

    /**
     * Do not call this method directly.  Use {@link #renderSelectionPreview()} instead.
     */
    private void runRenderSelectionPreview() {
        offscreenCanvas.setBitmap(selectionBox.getPreview());
        offscreenCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);

        offscreenCanvas.save();
        offscreenCanvas.translate(-selectionBox.getX(), -selectionBox.getY());
        for (BaseItem item : selectedItems) {
            item.render(offscreenCanvas, paint, context);
        }
        offscreenCanvas.restore();
    }

    /**
     * Renders selected items onto the offscreen canvas for sticky notes.
     */
    private void runRenderStickyNoteView(Bitmap bitmap) {
        offscreenCanvas.setBitmap(bitmap);
        offscreenCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);

        offscreenCanvas.save();
        offscreenCanvas.translate(-selectionBox.getX(), -selectionBox.getY());

        Input input = inputManager.getInput(currentDevice);

//        BaseItem item;
//        for (int i = selectedItems.size() - 1; i >= 0; i--) {
//            item = selectedItems.get(i);
        for (BaseItem item : selectedItems) {
            if (item instanceof Word) {
                ((Word) item).setSelectionAreaVisible(false);
            }
            item.setSelected(false);
            item.render(offscreenCanvas, paint, context);
            item.setSelected(true);
            if (item instanceof Word) {
                ((Word) item).setSelectionAreaVisible(input == Input.TEXT);
            }
        }
        offscreenCanvas.restore();
    }

    private void runRenderTrimmingView(Bitmap bitmap) {
        offscreenCanvas.setBitmap(bitmap);
        offscreenCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);

        offscreenCanvas.save();
        offscreenCanvas.translate(-selectionBox.getX(), -selectionBox.getY());

        Input input = inputManager.getInput(currentDevice);


        for (BaseItem item : selectedItems) {
            if (item instanceof Word) {
                ((Word) item).setSelectionAreaVisible(false);
            }
            item.setSelected(false);
            item.render(offscreenCanvas, paint, context);
            item.setSelected(true);
            if (item instanceof Word) {
                ((Word) item).setSelectionAreaVisible(input == Input.TEXT);
            }
        }
        offscreenCanvas.restore();
    }

    public void clearWhiteBoard(boolean b) {
        clearWhiteBoard = b;
        invalidateRender();
    }

    private void resetAutomaticPreviewUpdate() {
        lastStateChangeTime = System.currentTimeMillis();
    }

    private void resetTouchEventTimer() {
        lastTouchEventTime = System.currentTimeMillis();
    }

    /**
     * Renders a preview of the current state.
     */
    private void runRenderStatePreview(WhiteboardState state) {
        log("runRenderStatePreview");
        state.setPreviewDirty(false);

        boolean canvasOn = Props.getFlavorModel().equals(Props.Flavor.Model.Oem);

        int canvasW = canvasOn ? previewFileWidth : width;
        int canvasH = canvasOn ? previewFileHeight : height;

        final Bitmap statePreview = GlideApp.get(context)
                .getBitmapPool()
                .getDirty(canvasW, canvasH, Bitmap.Config.ARGB_8888);

        offscreenCanvas.setBitmap(statePreview);

        offscreenCanvas.save();

        // set up page background
        Drawable background = null;
        if (state.isPageCustomBackgroundSet()) {
            background = state.getPageCustomBackground();
        } else {
            final int pagePatternRes = state.getPagePatternRes();
            if (pagePatternRes != 0) {
                background = ContextCompat.getDrawable(context, pagePatternRes);
            }
        }

        if (canvasOn) {
            // get bounds of all items
            itemsRect.set(0, 0, width, height);
            for (BaseItem item : state.getItems()) {
                itemsRect.union(item.getLeft(), item.getTop(), item.getRight(), item.getBottom());
            }
            if (background != null) {
                setBackgroundBounds(background);
                final Rect bounds = background.getBounds();
                itemsRect.union(bounds.left, bounds.top, bounds.right, bounds.bottom);
            }

            final float scale = Math.min(1.0f * previewFileWidth / itemsRect.width(), 1.0f * previewFileHeight / itemsRect.height());
            offscreenCanvas.scale(scale, scale);

            // only translate if items are off to the left or top
            offscreenCanvas.translate(-Math.min(itemsRect.left, 0), -Math.min(itemsRect.top, 0));
        }

        // clear canvas
        final int pageColor = ContextCompat.getColor(context, state.getPageColorRes());
        offscreenCanvas.drawColor(pageColor);

        // draw page pattern
        if (background != null) {
            setBackgroundBounds(background);
            background.draw(offscreenCanvas);
        }

        // render each item, deselecting it first
        for (BaseItem item : state.getItems()) {
            final boolean isSelected = item.isSelected();
            if (isSelected) {
                item.setSelected(false);
            }

            boolean textSelectionAreaVisible = false;
            if (item instanceof Word) {
                textSelectionAreaVisible = ((Word) item).isSelectionAreaVisible();
                ((Word) item).setSelectionAreaVisible(false);
            }

            item.render(offscreenCanvas, paint, context);

            if (isSelected) {
                item.setSelected(true);
            }

            if (item instanceof Word) {
                ((Word) item).setSelectionAreaVisible(textSelectionAreaVisible);
            }
        }

        offscreenCanvas.restore();

        // save to file
        if (fileHandler != null && state.getPreviewFile() != null) {
            fileHandler.sendSaveImageMessage(statePreview, state.getPreviewFile(), null, true);
            state.setScreenshotTook(true);
        }

    }

    private void resetTouch() {
        fingerActiveCount = 0;

        selectionMode = SelectionMode.NONE;
        selectionBox.setImageTrimming(false);
        isTrimmingActive = false;
        isMultiFingerActive = false;
        isMultiTouchAllowed = true;

        if (eraserArea.isVisible()) {
            eraserArea.setVisible(false);
            invalidateRender();
        }

        if (!selectedItems.isEmpty()) {
            updateSelectionBox();
            renderSelectionPreview();
            selectionBox.setOutlineVisible(true);
            if (listener != null) {
                listener.onSelectionBoxVisibleChange(true, selectionBox.getSelectionRenderMode());
            }
            invalidateRender();
        }

        for (int i = 0; i < MAX_INPUT_POINTERS; i++) {
            pointerInputs[i] = null;
            downPoints[i].set(0, 0);
            dragPoints[i].set(0, 0);
        }

        if (activeItems.size() > 0) {
            for (int i = 0, size = activeItems.size(); i < size; i++) {
                final BaseItem item = activeItems.get(i);
                if (item != null) {
                    cacheQueue.add(item);
                    state.addItem(item);

                    Action action = Action.createInsertAction(item.getId());
                    state.addAction(action);
                }
            }

            activeItems.clear();
            renderCached();
        }

        if (listener != null) {
            listener.setMainThreadEraserAreaVisible(false, eraserSize, null);
            tempPointF.set(-40, -40);
            listener.setMainThreadEraserPosition(tempPointF);
        }

    }

    /**
     * Called when a touch down event occurs.
     */
    private void onTouchDown(int pointerIndex, int pointerId, float x, float y, MotionEvent event) {
        log("onTouchDown: pointerId=" + pointerId + ", x=" + x + ", y=" + y + ", device:" + getDevice(pointerId) +
                ", selected items size : " + selectedItems.size() + ", state items size : " + state.getItems().size());
        log("mode:" + selectionMode + ",isTrimmingActive:" + isTrimmingActive + ",isAllowMultiTouch:" + isMultiTouchAllowed);

        log("- state=" + (state != null));
        if (state == null) {
            return;
        }
        if (fingerActiveCount == 0) {
            log("reset touch at touchdown.");
            resetTouch();
        }

        x += scrollX;
        y += scrollY;

        Input input = getInputForPointer(pointerId);
        log("- input=" + input);

        fingerActiveCount = event.getPointerCount();

        if (input == null || !isInputAllowed(pointerId, input)) {
            return;
        }


        if (input != Input.SELECT) {
            state.setPreviewDirty(true);
            resetAutomaticPreviewUpdate();
        } else {
            if ((selectionMode == SelectionMode.ROTATE && fingerActiveCount > 1) || (fingerActiveCount > 2)) {
                log("ignored multiple touchdown cause:" + selectionMode.toString());
                for (int j = 0; j < MAX_INPUT_POINTERS; j++) {
                    if (pointerInputs[j] != null) {
                        pointerInputs[j] = Input.SELECT;
                        if (j != pointerId) {
                            fingerActiveCount = 1;
                            onTouchUp(j, dragPoints[j].x, dragPoints[j].y);
                            break;
                        }
                    }
                }
                selectionMode = SelectionMode.DISABLE;
                fingerActiveCount = event.getPointerCount() - 1;
                return;
            }
        }
        pointerInputs[pointerId] = input;

        final Device device = getDevice(pointerId);
        currentDevice = device;
        log("- currentDevice=" + currentDevice);

        if (!isTrimmingActive && selectionMode == SelectionMode.TRIMMING) {
            selectionMode = SelectionMode.NONE;
        }

        if (selectionPointerId == INVALID_POINTER_ID && !selectionLine.isEmpty()) {
            selectionLine.clear();
        }

        if (device == Device.PALM) {
            log("palm detected.");
            isMultiTouchAllowed = false;
            if (inputManager.getInput(Device.FINGER) == Input.SELECT || inputManager.getInput(Device.STYLUS) == Input.SELECT) {
                log("return cause earlier mode is SELECT.");
                for (int i = 0; i < MAX_INPUT_POINTERS; i++) {
                    if (pointerInputs[i] != null) {
                        pointerInputs[i] = Input.SELECT;
                        selectionMode = SelectionMode.DISABLE;
                        if (i != pointerId) {
                            fingerActiveCount = 1;
                            onTouchUp(i, dragPoints[i].x, dragPoints[i].y);
                        }
                    }
                }
                pointerInputs[pointerId] = null;
                selectionMode = SelectionMode.DISABLE;
                return;
            }
            if ((activeWord != null && activeWord.getTextStatus().equals(Word.TextStatus.FOCUSED))) {
                //update region of the active word before switch device to palm.
                activeWord.updateDetectRegion();
                clearTextBoxAndToggleKeyboard();
                renderCached();
            }

            fingerActiveCount--;
            for (int i = 0; i < MAX_INPUT_POINTERS; i++) {
                if (!dragPoints[i].equals(0, 0) && getDevice(i) != Device.PALM) {
                    sendPointerTouchUp(i);
                }
            }
            fingerActiveCount++;

            if (isMultiFingerActive) {
                return;
            }

            disableTrimmingMode();
        }

        if (fingerActiveCount == 1 && pointerId == 0) {
            firstFingerDownTime = event.getEventTime();
            log("- firstFingerDownTime set: " + firstFingerDownTime);
        }
        // ----------------------------------------------------------------------------------------------------------------------------------

        if (input != Input.TEXT && (activeWord != null && activeWord.getTextStatus().equals(Word.TextStatus.FOCUSED))) {
            //update region of the active word before switch device to palm.
            activeWord.updateDetectRegion();
            clearTextBoxAndToggleKeyboard();
            renderCached();
        }

        if (input == Input.TEXT && pointerId > 0) {
            pointerInputs[pointerId] = null;
            return;
        }

//        selectionBox.setTextScaleVisible(input == Whiteboard.Input.TEXT);

        // delete empty word, hide text selection area
        for (int i = 0; i < state.getItems().size(); i++) {
            BaseItem a = state.getItems().get(i);
            if (a instanceof Word) {
                Word w = (Word) a;
                w.setSelectionAreaVisible(input == Whiteboard.Input.TEXT);

                if (w.getTextStatus().equals(Word.TextStatus.FOCUSED) && input != Whiteboard.Input.TEXT) {
                    w.setTextStatus(Word.TextStatus.UNFOCUSED);
                }

//                if (w.getText().length() <= 0 && !w.hit(x, y)) {
                if (w.getText().isEmpty()) {
                    w.setTextStatus(Word.TextStatus.UNFOCUSED);// remove view from root
                    if (w.getEdited()) {
                        w.saveWordState();
                        Action textRemove = Action.createDeleteTextAction(i, w);
                        state.addAction(textRemove);
                    }
                    state.removeItem(w);
                }
            }
        }

        if (input != Input.SELECT && input != Whiteboard.Input.TEXT && state.hasSelectedItems()) {
            state.clearSelected();
            selectedItems.clear();
            selectionBox.hide();
            if (listener != null) {
                listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
            }
            renderCached();
        }

        if (listener != null) {
            listener.onCurrentInputChange(device, input);
        }

        boolean requestInvalidate = true;

        switch (input) {
            case MOVE_CANVAS: {
                isscale = true;
                first = true;
                if (multiFingerCount == -1 || state.isAnnotationState()) {
                    break;
                }

//                if (isTrimmingActive) {
//                    applyTrimming();
//                    disableTrimmingMode();
//                    break;
//                }

                if (fingerActiveCount >= 1) {
                    // Zoom
                    inTouchSlop = true;
                    activeItems.clear();
                    multiFingerCount = fingerActiveCount;
                    cleanFrameBuffer();

                    zDownPoints[pointerId].set(x - scrollX, y - scrollY);
                    zDragPoints[pointerId].set(x - scrollX, y - scrollY);
                    zLastDragPoints[pointerId].set(x - scrollX, y - scrollY);

                    zoomPoint.x = 0;
                    zoomPoint.y = 0;
                    int points = 0;
                    for (int i = 0; i < MAX_INPUT_POINTERS; i++) {
                        if (pointerInputs[i] == Input.MOVE_CANVAS) {
                            points++;

                            zDownPoints[i].set(zDragPoints[i]);

                            zoomPoint.x += zDownPoints[i].x;
                            zoomPoint.y += zDownPoints[i].y;
                        }
                    }

                    if (points > 0) {
                        zoomPoint.x /= points;
                        zoomPoint.y /= points;
                        zoomDownPoint.set(zoomPoint);
                        scrollPointerId = pointerId;
                    }
                }

                break;
            }
            case AUTOSHAPE: {
                final Autoshape autoshape = autoshapeProperty.create(context, x, y, 1);
                activeItems.put(pointerId, autoshape);
                break;
            }
            case OBJECT_ERASER: {
                if (fingerActiveCount == 1) {
//                        eraserPointerId = pointerId;
                    objectEraserPen = new ObjectEraserPen(context, x, y, context.getResources().getDimensionPixelSize(R.dimen.eraser_object_default_thickness));
                    listener.setMainThreadEraserAreaVisible(true, context.getResources().getDimensionPixelSize(R.dimen.eraser_vertical_offset), input);
                    isMultiTouchAllowed = false;

                } else {
                    if (listener != null)
                        listener.setMainThreadEraserAreaVisible(false, context.getResources().getDimensionPixelSize(R.dimen.eraser_vertical_offset), input);
                }
                break;
            }
            case PARTIAL_ERASER: {
                if (fingerActiveCount == 1) {
                    eraserSize = getEraserSize(device);
                    partialEraserPen = new PartialEraserPen(context, x, y, getEraserSize(device));
                    partialEraserPen.setBitmapShader(getEraserScratchCached());
                    listener.onCurrentInputChange(device, input);
//                        listener.setMainThreadEraserAreaVisible(true, eraserSize);
                }
                if (listener != null) {
                    listener.setMainThreadEraserAreaVisible(false, eraserSize, input);
                }
                isMultiTouchAllowed = false;
                break;
            }
            case PEN:
            case HIGHLIGHTER: {
                if (fastDraw.isDrawing()) {
                    requestInvalidate = false;

                    final List<PointF> points = pointsList.get(pointerId);
                    points.clear();
                    points.add(new PointF(x, y));
                }
                if (input != Input.PEN) {
                    log("highlighter touch down.");
                    activeItems.put(pointerId, device == Device.FINGER
                            ? fingerHighlighterProperty.create(context, x, y, this.state.getAppliedScale())
                            : stylusHighlighterProperty.create(context, x, y, this.state.getAppliedScale()));
                } else {
                    log("pen touch down.");
                    activeItems.put(pointerId, device == Device.FINGER
                            ? fingerPenProperty.create(context, x, y, this.state.getAppliedScale())
                            : stylusPenProperty.create(context, x, y, this.state.getAppliedScale()));
                }


                break;
            }
//            case PEN: {
//                if (fastDraw.isDrawing()) {
//                    requestInvalidate = false;
//
//                    final List<PointF> points = pointsList.get(pointerId);
//                    points.clear();
//                    points.add(new PointF(x, y));
//                } else {
//                    activeItems.put(pointerId, device == Device.FINGER
//                            ? fingerPenProperty.create(context, x, y)
//                            : stylusPenProperty.create(context, x, y));
//                }
//                break;
//            }
            case SELECT: {
                log("- input: SELECT");
                log("- fingerActiveCount=" + fingerActiveCount);
                if (!Props.getFlavorChipset().equals(Props.Flavor.Chipset.Amlogic))
                    SystemPropertiesUtils.setValue(SystemPropertiesUtils.KEY_FRAME_BUFFER_LOCK, LOCK_FRAME_BUFFER_ON);
                if (fingerActiveCount == 1) {
                    selectionPointerId = pointerId;
                    selectionLine.setVisible(true);
                    boolean clearSelection = true;

                    if (selectionBox.isVisible()) {
                        if (selectedItems.size() == 1 && selectedItems.get(0) instanceof Word) {
                            Word word = (Word) selectedItems.get(0);
                            selectionControl = selectionBox.hitControls(word.getTextBoxBounds(), x, y, word.getRotation());
                        } else {
                            selectionControl = selectionBox.hitControls(x, y);
                        }
                        if (!selectionBox.getHeadTailVisible() && (selectionControl == SelectionBox.Control.HEADREDIRECT || selectionControl == SelectionBox.Control.TAILREDIRECT)) {
                            selectionControl = SelectionBox.Control.NONE;
                        }
                        switch (selectionControl) {
                            case SelectionBox.Control.ROTATE: {
                                transform.clear();


                                selectionBox.setPreviewVisible(true);
                                selectionBox.setOutlineVisible(false);

                                if (listener != null) {
                                    listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                                }

                                final float centerX = selectionBox.getX() + selectionBox.getWidth() / 2;
                                final float centerY = selectionBox.getY() + selectionBox.getHeight() / 2;
                                selectionBox.setRotatePoint(centerX, centerY);
                                rotateAngleOffset = getAngle(centerX, centerY, x, y);

                                selectionMode = SelectionMode.ROTATE;
                                clearSelection = false;
                                break;
                            }
                            case SelectionBox.Control.HANDLES_LEFT_TOP: {
                                transform.clear();
                                if (listener != null) {
                                    listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                                }
                                selectionMode = SelectionMode.SCALE;
//                                renderSelectionPreview();// update text box preview by a filled rectangle;
                                selectionBox.setScalePoint(SelectionBox.Control.HANDLES_LEFT_TOP);
                                selectionBox.setPreviewVisible(true);
                                selectionBox.setOutlineVisible(false);
                                clearSelection = false;
                                break;
                            }
                            case SelectionBox.Control.HANDLES_LEFT_BOTTOM: {
                                transform.clear();
                                if (listener != null) {
                                    listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                                }
                                selectionMode = SelectionMode.SCALE;
                                selectionBox.setScalePoint(SelectionBox.Control.HANDLES_LEFT_BOTTOM);
                                selectionBox.setPreviewVisible(true);
                                selectionBox.setOutlineVisible(false);
                                clearSelection = false;
                                break;
                            }
                            case SelectionBox.Control.HANDLES_RIGHT_BOTTOM: {
                                transform.clear();
                                if (listener != null) {
                                    listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                                }
                                selectionMode = SelectionMode.SCALE;
                                selectionBox.setScalePoint(SelectionBox.Control.HANDLES_RIGHT_BOTTOM);
                                selectionBox.setPreviewVisible(true);
                                selectionBox.setOutlineVisible(false);
                                clearSelection = false;
                                break;
                            }
                            case SelectionBox.Control.HANDLES_RIGHT_TOP: {
                                transform.clear();
                                if (listener != null) {
                                    listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                                }
                                selectionMode = SelectionMode.SCALE;
                                selectionBox.setScalePoint(SelectionBox.Control.HANDLES_RIGHT_TOP);
                                selectionBox.setPreviewVisible(true);
                                selectionBox.setOutlineVisible(false);
                                clearSelection = false;
                                break;
                            }
                            case SelectionBox.Control.TRIM_LEFT_BOTTOM: {
                                if (listener != null) {
                                    listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                                }
                                selectionBox.setTrimingCorner(SelectionBox.Control.HANDLES_LEFT_BOTTOM);
                                clearSelection = false;
                                break;
                            }
                            case SelectionBox.Control.TRIM_LEFT_TOP: {
                                if (listener != null) {
                                    listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                                }
                                selectionBox.setTrimingCorner(SelectionBox.Control.HANDLES_LEFT_TOP);
                                clearSelection = false;
                                break;
                            }
                            case SelectionBox.Control.TRIM_RIGHT_BOTTOM: {
                                if (listener != null) {
                                    listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                                }
                                selectionBox.setTrimingCorner(SelectionBox.Control.HANDLES_RIGHT_BOTTOM);
                                clearSelection = false;
                                break;
                            }
                            case SelectionBox.Control.TRIM_RIGHT_TOP: {
                                if (listener != null) {
                                    listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                                }
                                selectionBox.setTrimingCorner(SelectionBox.Control.HANDLES_RIGHT_TOP);
                                clearSelection = false;
                                break;
                            }
                            case SelectionBox.Control.HANDLES_LEFT: {
                                transform.clear();
                                if (listener != null) {
                                    listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                                }
                                selectionMode = SelectionMode.SCALE;
                                selectionBox.setScalePoint(SelectionBox.Control.HANDLES_LEFT);
                                selectionBox.setPreviewVisible(true);
                                selectionBox.setOutlineVisible(false);
                                clearSelection = false;
                                break;
                            }
                            case SelectionBox.Control.HANDLES_TOP: {
                                transform.clear();
                                if (listener != null) {
                                    listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                                }
                                selectionMode = SelectionMode.SCALE;
                                selectionBox.setScalePoint(SelectionBox.Control.HANDLES_TOP);
                                selectionBox.setPreviewVisible(true);
                                selectionBox.setOutlineVisible(false);
                                clearSelection = false;
                                break;
                            }
                            case SelectionBox.Control.HANDLES_RIGHT: {
                                transform.clear();
                                if (listener != null) {
                                    listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                                }
                                selectionMode = SelectionMode.SCALE;
                                selectionBox.setScalePoint(SelectionBox.Control.HANDLES_RIGHT);
                                selectionBox.setPreviewVisible(true);
                                selectionBox.setOutlineVisible(false);
                                clearSelection = false;
                                break;
                            }
                            case SelectionBox.Control.HANDLES_BOTTOM: {
                                transform.clear();
                                if (listener != null) {
                                    listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                                }
                                selectionMode = SelectionMode.SCALE;
                                selectionBox.setScalePoint(SelectionBox.Control.HANDLES_BOTTOM);
                                selectionBox.setPreviewVisible(true);
                                selectionBox.setOutlineVisible(false);
                                clearSelection = false;
                                break;
                            }
                            case SelectionBox.Control.HEADREDIRECT: {
                                transform.clear();
                                if (listener != null) {
                                    listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                                }
                                selectionMode = SelectionMode.REDIRECT;
                                // set rotate point in the case of redirection
                                selectionBox.setScalePoint(SelectionBox.Control.HEADREDIRECT);
                                rotateAngleOffset = getAngle(selectionBox.getScalePoint().x, selectionBox.getScalePoint().y, x, y);
                                selectionBox.setPreviewVisible(true);
                                selectionBox.setOutlineVisible(false);
                                clearSelection = false;
                                break;
                            }
                            case SelectionBox.Control.TAILREDIRECT: {
                                transform.clear();
                                if (listener != null) {
                                    listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                                }
                                selectionMode = SelectionMode.REDIRECT;
                                // set rotate point in the case of redirection
                                selectionBox.setScalePoint(SelectionBox.Control.TAILREDIRECT);
                                rotateAngleOffset = getAngle(selectionBox.getScalePoint().x, selectionBox.getScalePoint().y, x, y);
                                selectionBox.setPreviewVisible(true);
                                selectionBox.setOutlineVisible(false);
                                clearSelection = false;
                                break;
                            }
                            case SelectionBox.Control.HANDLES_TEXT_SCALE: {
                                transform.clear();
                                selectionBox.setOutlineVisible(false);
                                if (listener != null) {
                                    listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                                }
                                Word word = (Word) selectedItems.get(0);
                                word.quickSaveWordState();
                                word.setFirstDragPadding(selectionBox.getFirstDragPadding());
                                word.prepareScalingTextBoxPreview();
                                selectionMode = SelectionMode.TEXT_SCALE;
                                clearSelection = false;
                                break;
                            }
                            case SelectionBox.Control.NONE: {
                                if (selectionMode == SelectionMode.TRIMMING) {
                                    final Bitmap bitmap = Bitmap.createBitmap((int) selectionBox.getWidth(),
                                            (int) selectionBox.getHeight(), Bitmap.Config.ARGB_8888);
                                    runRenderTrimmingView(bitmap);
                                    Bitmap resultBitmap = Bitmap.createBitmap(bitmap, Math.abs((int) (selectionBox.getTrimRectF().left - selectionBox.getX())), Math.abs((int) (selectionBox.getTrimRectF().top - selectionBox.getY())),
                                            (int) selectionBox.getTrimRectF().width(), (int) selectionBox.getTrimRectF().height());
                                    addTrimmedImage(resultBitmap, selectionBox.getTrimRectF().left, selectionBox.getTrimRectF().top);
                                    disableTrimmingMode();
                                    clearSelection = true;
                                    break;
                                }

                                if (selectionBox.hit(x, y)) {
                                    transform.clear();

                                    selectionBox.setPreviewVisible(true);
                                    selectionBox.setOutlineVisible(false);

                                    if (listener != null) {
                                        listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                                    }

                                    selectionMode = SelectionMode.MOVE;
                                    clearSelection = false;
                                } else {

                                    clearSelection = true;
                                }
                                break;
                            }
                        }
                    }

                    if (clearSelection) {
                        state.clearSelected();
                        selectedItems.clear();
                        selectionBox.hide();
                        if (listener != null) {
                            listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                        }
                        renderCached();

                        selectionLine.stopAreaAnimation();
                    }
                } else if (fingerActiveCount == 2) {
                    if (selectionMode == SelectionMode.MOVE) {
                        selectionMode = SelectionMode.ROTATE_AND_SCALE;
                        // use tempPointF to record translate difference
                        tempPointF.set(dragPoints[0].x - downPoints[0].x, dragPoints[0].y - downPoints[0].y);
                        transform.setTranslate(dragPoints[0].x - downPoints[0].x,
                                dragPoints[0].y - downPoints[0].y);
                        // force selected word to save its current state before rotate_and_scale
                        for (BaseItem item : state.getItems()) {
                            if (item instanceof Word && item.isSelected()) {
                                ((Word) item).quickSaveWordState();
                            }
                        }
                        transform.apply(selectedItems);
                        transform.clear();
                        downPoints[0].set(dragPoints[0]);

                        renderSelectionPreview();
                        final float centerX = (downPoints[0].x + x) / 2;
                        final float centerY = (downPoints[0].y + y) / 2;

                        selectionBox.setRotatePoint(centerX, centerY);
                        selectionBox.setScalePoint(centerX, centerY);

                        rotateAngleOffset = getAngle(downPoints[0].x, downPoints[0].y, x, y);
                    }

                } else {
                    if (selectionMode != SelectionMode.DISABLE) {
                        jobsBeforeSuspend();
                        selectionMode = SelectionMode.DISABLE;
                    }
                }
                break;
            }
            case TEXT: {
                if (pointerId != 0) {
                    break;
                }

                if (activeWord != null && (!state.getItems().contains(activeWord) || activeWord.getTextStatus().equals(Word.TextStatus.UNFOCUSED))) {// activeWord is removed by delete action / cut action / ...
                    setActiveWord(null);
                }

                boolean hasDeselected = false;
                boolean hit = false;
                boolean updateCache = false;

                boolean isSelected = (activeWord != null && (activeWord.getTextStatus().equals(Word.TextStatus.SELECTED)));

                if (activeWord != null) {
                    activeWord.updateDetectRegion();
                }
                //textbox status control
//                if (activeWord == null ||
//                        (!activeWord.hit(x, y) && !activeWord.hitBorder(x, y)
//                        && !(selectionBox.isVisible() && selectionBox.hitControls(x, y) != SelectionBox.Control.NONE))) {
                if (activeWord == null || (
                        ((isSelected && !activeWord.hitBox(x, y)) ||
                                (!isSelected && !activeWord.hitArea(x, y) && !activeWord.hitBorder(x, y))) &&
                                !(selectionBox.isVisible() && selectionBox.hitControls(x, y) != SelectionBox.Control.NONE))) {

                    final List<BaseItem> items = state.getItems();

                    for (int i = items.size() - 1; i >= 0; i--) {
                        if (items.get(i) instanceof Word && ((Word) items.get(i)).getEdited()) {
                            Word w = (Word) items.get(i);
                            Word.TextStatus status = w.getTextStatus();
                            w.updateDetectRegion();

                            if (!hit) {
                                if (status.equals(Word.TextStatus.SELECTED)) {
                                    hit = w.hitBox(x, y);
                                } else {
                                    hit = (w.hitArea(x, y) || w.hitBorder(x, y));
                                }
                                if (hit) {
                                    // will call setOnActiveWord(null, false) if w is selected
                                    activeWord = w;
                                    continue;
                                }
                            }

                            if (!hasDeselected && !status.equals(Word.TextStatus.UNFOCUSED)) {
                                hasDeselected = true;
                            }
                            w.setTextStatus(Word.TextStatus.UNFOCUSED);
                        } else {
                            // TODO : recheck
                            if (!(items.get(i) instanceof Word)) {
                                items.get(i).setSelected(false);
                            }
                        }
                    }

                    if (!hit && activeWord != null) {
                        setActiveWord(null);
                    }
                    updateCache = true;
                }
                selectedItems.clear();

                if (activeWord != null) {
                    Word.TextStatus currentStatus = activeWord.getTextStatus();
                    if (!currentStatus.equals(Word.TextStatus.SELECTED)) {
                        if (activeWord.hitArea(x, y)) {
                            activeWord.setTextStatus(Word.TextStatus.FOCUSED);
                            updateCache = true;
                        } else if (activeWord.hitBorder(x, y)) {
                            activeWord.setTextStatus(Word.TextStatus.SELECTED);
                            selectedItems.add(activeWord);
                            updateCache = true;
                        } else {
                            log("Error, hit : " + activeWord.hit(x, y) + ", hitArea : " + activeWord.hitArea(x, y) + ", hitBorder : " + activeWord.hitBorder(x, y));
                        }
                    } else {
                        selectedItems.add(activeWord);
                    }
                    // SELECTED -> SELECTED because textBox is hit/hitArea/hitBorder

                } else {
                    if (!hasDeselected) {
                        if (x >= 0 && y >= 0) {
                            final Word word = wordProperty.createText(context, x, y);
                            word.setTextStatus(Word.TextStatus.FOCUSED);
                            state.addItem(word);
                            activeItems.put(pointerId, word);
                            activeWord = word;
                        } else {
                            log("Error : when new a textbox at wrong position (x,y) : (" + x + ", " + y + ")");
                        }
                    }
                }

                // keyboard control and request focus of activeWord
                if (activeWord != null && activeWord.getTextStatus().equals(Word.TextStatus.FOCUSED)) {
                    setActiveWord(activeWord);
                    activeWord.setCursor(event);
//                    setCursor(event, activeWord);
                    setCursor(activeWord.getCursor());
                }

                // set select mode and prepare for the action
                // directly copy following lines from case SELECT
                boolean clearSelection = true;
                if (selectionBox.isVisible() && (activeWord != null && activeWord.getTextStatus().equals(Word.TextStatus.SELECTED))) {
                    activeWord.quickSaveWordState();
//                    updateSelectionBox();
//                    selectionBox.updateScaleButtonBound(activeWord.getTextBoxBounds(), activeWord.getRotation());
//                    switch (selectionBox.hitControls(x, y)) {
                    switch (selectionBox.hitControls(activeWord.getTextBoxBounds(), x, y, activeWord.getRotation())) {

                        case SelectionBox.Control.ROTATE: {
                            transform.clear();

                            selectionBox.setPreviewVisible(true);
                            selectionBox.setOutlineVisible(false);

                            if (listener != null) {
                                listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                            }

                            final float centerX = selectionBox.getX() + selectionBox.getWidth() / 2;
                            final float centerY = selectionBox.getY() + selectionBox.getHeight() / 2;
                            selectionBox.setRotatePoint(centerX, centerY);
                            rotateAngleOffset = getAngle(centerX, centerY, x, y);

                            selectionMode = SelectionMode.ROTATE;
                            clearSelection = false;
                            break;
                        }
                        case SelectionBox.Control.HANDLES_LEFT_TOP: {
                            transform.clear();
                            if (listener != null) {
                                listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                            }
                            selectionMode = SelectionMode.SCALE;
                            selectionBox.setScalePoint(SelectionBox.Control.HANDLES_LEFT_TOP);
                            selectionBox.setPreviewVisible(true);
                            selectionBox.setOutlineVisible(false);
                            clearSelection = false;
                            break;
                        }
                        case SelectionBox.Control.HANDLES_LEFT_BOTTOM: {
                            transform.clear();
                            if (listener != null) {
                                listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                            }
                            selectionMode = SelectionMode.SCALE;
                            selectionBox.setScalePoint(SelectionBox.Control.HANDLES_LEFT_BOTTOM);
                            selectionBox.setPreviewVisible(true);
                            selectionBox.setOutlineVisible(false);
                            clearSelection = false;
                            break;
                        }
                        case SelectionBox.Control.HANDLES_RIGHT_BOTTOM: {
                            transform.clear();
                            if (listener != null) {
                                listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                            }
                            selectionMode = SelectionMode.SCALE;
                            selectionBox.setScalePoint(SelectionBox.Control.HANDLES_RIGHT_BOTTOM);
                            selectionBox.setPreviewVisible(true);
                            selectionBox.setOutlineVisible(false);
                            clearSelection = false;
                            break;
                        }
                        case SelectionBox.Control.HANDLES_RIGHT_TOP: {
                            transform.clear();
                            if (listener != null) {
                                listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                            }
                            selectionMode = SelectionMode.SCALE;
                            selectionBox.setScalePoint(SelectionBox.Control.HANDLES_RIGHT_TOP);
                            selectionBox.setPreviewVisible(true);
                            selectionBox.setOutlineVisible(false);
                            clearSelection = false;
                            break;
                        }
                        case SelectionBox.Control.HANDLES_TEXT_SCALE: {
                            transform.clear();
                            selectionBox.setOutlineVisible(false);
                            if (listener != null) {
                                listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                            }
                            activeWord.setFirstDragPadding(selectionBox.getFirstDragPadding());
                            activeWord.prepareScalingTextBoxPreview();
                            updateCache = false;
                            selectionMode = SelectionMode.TEXT_SCALE;
                            clearSelection = false;
                            break;
                        }
                        case SelectionBox.Control.NONE: {
                            transform.clear();
                            selectionBox.setPreviewVisible(true);
                            selectionBox.setOutlineVisible(false);
                            if (listener != null) {
                                listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                            }
                            updateCache = true;

                            selectionMode = SelectionMode.MOVE;
                            clearSelection = false;
                            break;
                        }
                    }
                }

                if (clearSelection && selectionBox.isVisible() && (activeWord == null || !activeWord.getTextStatus().equals(Word.TextStatus.SELECTED))) {
//                    state.clearSelected();
                    // DO NOT DIRECTLY SET SELECTED TO WORD, remind me do not create the same mistake here
                    for (BaseItem item : state.getItems()) {
                        if (item instanceof Word && ((Word) item).getTextStatus().equals(Word.TextStatus.SELECTED)) {
                            ((Word) item).setTextStatus(Word.TextStatus.UNFOCUSED);
                        }
                    }
                    selectedItems.clear();
                    selectionBox.hide();
                    if (listener != null) {
                        listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                    }
                    updateCache = true;
                }

                if (updateCache) {
                    renderCached();
                }

                // hint text control
                setShowText(activeWord == null);
                break;
            }
        }


        downPoints[pointerId].set(x, y);
        dragPoints[pointerId].set(x, y);
        lastDragPoints[pointerId].set(x, y);

        if (requestInvalidate) {
            invalidateRender();
        }
    }

    /**
     * Called when a touch drag event occurs.
     */
    private void onTouchDrag(int pointerIndex, int pointerId, float x, float y, long time) {
        isscale = true;
//        log("onTouchDrag: pointerId=" + pointerId + ", x=" + x + ", y=" + y + ", device:" + getDevice(pointerId));
        if (state == null) {
            return;
        }

        x += scrollX;
        y += scrollY;

        Input input = pointerInputs[pointerId];

        if (input == null || !isInputAllowed(pointerId, input)) {
            return;
        }

        dragPoints[pointerId].set(x, y);

//        if (input == Input.OBJECT_ERASER) {
//            lastDragPoints[pointerId].set(dragPoints[pointerId].x, dragPoints[pointerId].y);
//        }

        boolean requestInvalidate = true;

        switch (input) {
            case AUTOSHAPE: {
                final BaseItem activeItem = activeItems.get(pointerId);
                if (activeItem instanceof Autoshape) {
                    Autoshape autoshape = (Autoshape) activeItem;
                    switch (autoshape.getType()) {
                        case CIRCLE: {
                            tempPointF.set(downPoints[pointerId]);
                            tempPointF.offset(-dragPoints[pointerId].x, -dragPoints[pointerId].y);
                            autoshape.circleSet((downPoints[pointerId].x + dragPoints[pointerId].x) / 2,
                                    (downPoints[pointerId].y + dragPoints[pointerId].y) / 2, tempPointF.length());

                            break;
                        }
                        case LINE: {
                            autoshape.linePointSet(downPoints[pointerId], dragPoints[pointerId]);
                            break;
                        }
                        case OVAL: {
                            autoshape.ovalSet(
                                    Math.min(downPoints[pointerId].x, dragPoints[pointerId].x),
                                    Math.min(downPoints[pointerId].y, dragPoints[pointerId].y),
                                    Math.abs(downPoints[pointerId].x - dragPoints[pointerId].x),
                                    Math.abs(downPoints[pointerId].y - dragPoints[pointerId].y));
                            break;
                        }
                        case SQUARE: {
                            autoshape.squareSet(
                                    Math.min(downPoints[pointerId].x, dragPoints[pointerId].x),
                                    Math.min(downPoints[pointerId].y, dragPoints[pointerId].y),
                                    Math.abs(downPoints[pointerId].x - dragPoints[pointerId].x),
                                    Math.abs(downPoints[pointerId].y - dragPoints[pointerId].y));
                            break;
                        }
                        case REGULAR_TRIANGLE: {
                            autoshape.regularTriangleSet(
                                    downPoints[pointerId].x,
                                    downPoints[pointerId].y,
                                    dragPoints[pointerId].x,
                                    dragPoints[pointerId].y);
                            break;
                        }
                        case RIGHT_TRIANGLE: {
                            autoshape.rightTriangleSet(
                                    downPoints[pointerId].x, downPoints[pointerId].y,
                                    dragPoints[pointerId].x, dragPoints[pointerId].y,
                                    downPoints[pointerId].x, dragPoints[pointerId].y
                            );
                            break;
                        }
                        case STAR: {
                            autoshape.starSet(
                                    (downPoints[pointerId].x + dragPoints[pointerId].x) / 2,
                                    downPoints[pointerId].y,
                                    Math.abs(dragPoints[pointerId].x - downPoints[pointerId].x),

                                    downPoints[pointerId], dragPoints[pointerId]
                            );
                            break;
                        }
                        case ARROW: {
                            autoshape.arrowSet(downPoints[pointerId], dragPoints[pointerId]);
                            break;
                        }
                    }
                }
                break;
            }
            case MOVE_CANVAS: {
                if (multiFingerCount == -1 || state.isAnnotationState()) {
                    break;
                }

                if (fingerActiveCount >= 1) {
                    isMultiFingerActive = true;

                    float rate = 1.0f;

                    final float absoluteX = x - scrollX;
                    final float absoluteY = y - scrollY;

                    zDragPoints[pointerId].set(absoluteX, absoluteY);

                    if (justTouchUp) {
                        justTouchUp = false;
                        for (int i = 0; i < MAX_INPUT_POINTERS; i++) {
                            if (pointerInputs[i] == Input.MOVE_CANVAS) {
                                zDownPoints[i].set(zDragPoints[i]);
                                zLastDragPoints[i].set(zDragPoints[i]);
                            }
                        }
                    }

                    boolean stop = false;
                    if (fingerActiveCount >= 2) {
                        float oldLengthSum = 0;
                        float newLengthSum = 0;
                        for (int i = 0; i < MAX_INPUT_POINTERS; i++) {
                            if (pointerInputs[i] == Input.MOVE_CANVAS) {
                                for (int j = i + 1; j < MAX_INPUT_POINTERS; j++) {
                                    if (pointerInputs[j] == Input.MOVE_CANVAS) {
                                        float oldWidth = zDownPoints[j].x - zDownPoints[i].x;
                                        float oldHeight = zDownPoints[j].y - zDownPoints[i].y;
                                        float newWidth = zDragPoints[j].x - zDragPoints[i].x;
                                        float newHeight = zDragPoints[j].y - zDragPoints[i].y;

                                        oldLength.set(oldWidth, oldHeight);
                                        newLength.set(newWidth, newHeight);


                                        if ((oldLength.length() * oldLength.length()) <= touchSlopDistance ||
                                                (newLength.length() * newLength.length()) <= touchSlopDistance) {
                                            stop = true;
                                            break;
                                        }

                                        oldLengthSum += oldLength.length();
                                        newLengthSum += newLength.length();
                                        i = j - 1;
                                        break;
                                    }
                                }
                            }

                            if (stop) {
                                break;
                            }
                        }

                        if (stop) {
                            break;
                        }

                        if (newLengthSum > 0 && oldLengthSum > 0) {

                            rate = newLengthSum / oldLengthSum;

                            float totalScale = rate * appliedScale * bgzoomScales[MainActivity.stateIndex];
                            if (Float.isNaN(rate) || Float.isInfinite(rate)) {
                                rate = 1.0f;
                            } else if (totalScale > MAX_ZOOM) {
                                rate = MAX_ZOOM / appliedScale;
                            } else if (totalScale < MIN_ZOOM) {
                                rate = MIN_ZOOM / appliedScale;
                            }
                        }
                    }

                    zoomPoint.x = 0;
                    zoomPoint.y = 0;
                    int points = 0;
                    float dragX = 0;
                    float dragY = 0;
                    float dragbgX = 0;
                    float dragbgY = 0;
                    for (int i = 0; i < MAX_INPUT_POINTERS; i++) {
                        if (pointerInputs[i] == Input.MOVE_CANVAS) {
                            points++;
                            zoomPoint.x += zDragPoints[i].x;
                            zoomPoint.y += zDragPoints[i].y;
                            dragX += (zDragPoints[i].x - zLastDragPoints[i].x);
                            dragY += (zDragPoints[i].y - zLastDragPoints[i].y);
                            dragbgX += (zDragPoints[i].x - zLastDragPoints[i].x);
                            dragbgY += (zDragPoints[i].y - zLastDragPoints[i].y);
                        }
                    }

                    if (points > 0) {
                        zoomPoint.x /= points;
                        zoomPoint.y /= points;

                        dragX /= points;
                        dragY /= points;

                        dragbgX /= points;
                        dragbgY /= points;

                        dragX *= zoomScale;
                        dragY *= zoomScale;

                        dragbgX *= bgzoomScales[MainActivity.stateIndex];
                        dragbgY *= bgzoomScales[MainActivity.stateIndex];

                        scrollX += dragX;
                        scrollY += dragY;

                        scrollbgXs[MainActivity.stateIndex] += dragbgX;
                        scrollbgYs[MainActivity.stateIndex] += dragbgY;
                    }

                    if (inTouchSlop) {
//                        if (Math.abs(rate - 1.0f) > 0.02f) {
                        if (Math.abs(rate - 1.0f) > 0.08f) {
                            inTouchSlop = false;
                        }
                    }

                    float totalScale = rate * appliedScale * zoomScale;

                    if (!inTouchSlop) {
                        if (!Float.isNaN(rate)) {
                            if (isscale && rate != 1 && totalScale <= 3 && totalScale >= 0.75) {
                                zoomScale = rate;
                                bgzoomScales[MainActivity.stateIndex] = rate;
                            } else {
                                if (totalScale > 3) {
                                    hintText = String.format(Locale.US, "%d%%", (int) (300));
                                    hintTextLength = paint.measureText(hintText);
                                } else if (totalScale < 0.75) {
                                    hintText = String.format(Locale.US, "%d%%", (int) 75);
                                    hintTextLength = paint.measureText(hintText);
                                }
                            }
                        }

                        if (fingerActiveCount > 1 && (totalScale >= 0.75f && totalScale <= 3.00f)) {
                            showHintText = true;
                            hintText = String.format(Locale.US, "%d%%", (int) (totalScale * 100));
                            hintTextLength = paint.measureText(hintText);
                        }
//                        if (fingerActiveCount > 1 && (rate * appliedScale <= 0.75f || rate * appliedScale >= 3.00f)) {
//                            showHintText = true;
//                            hintText = String.format(Locale.US, "%d%%", (int) (rate * appliedScale * 100));
//                            hintTextLength = paint.measureText(hintText);
//                        }
                    } else {
                        if (fingerActiveCount > 1 && (totalScale >= 0.75f && totalScale <= 3.00f)) {
                            showHintText = true;
                            hintText = String.format(Locale.US, "%d%%", (int) (totalScale * 100));
                            hintTextLength = paint.measureText(hintText);
                        }
//                        if (fingerActiveCount > 1 && (rate * appliedScale <= 0.75f || rate * appliedScale >= 3.00f)) {
//                            showHintText = true;
//                            hintText = String.format(Locale.US, "%d%%", (int) (rate * appliedScale * 100));
//                            hintTextLength = paint.measureText(hintText);
//                        }
                    }

                    zLastDragPoints[pointerId].set(absoluteX, absoluteY);

                }
                break;
            }
            case OBJECT_ERASER: {
                if (fingerActiveCount == 1) {
                    if (objectEraserPen != null)
                        objectEraserPen.add(x, y);
                    if (listener != null) {
                        listener.setMainThreadEraserAreaVisible(true, 0, input);
                    }
                }
                break;
            }
            case PARTIAL_ERASER: {
                if (!FastDraw.isSupportedCanvasFastDrawEraser()) {
                    if (pointerId == 0) {
                        if (partialEraserPen != null) {
                            partialEraserPen.add(x, y);
                        }

                        if (listener != null) {
                            listener.setMainThreadEraserAreaVisible(true, eraserSize, input);
                        }
                    }
                } else {
                    if (fingerActiveCount == 1 && partialEraserPen != null) {
                        partialEraserPen.add(x, y);
                        if (listener != null) {
                            listener.setMainThreadEraserAreaVisible(true, eraserSize, input);
                        }
                    }
                }
                break;
            }
            case PEN:
            case HIGHLIGHTER: {
//                if (fastDraw.isDrawing()) {
//                    requestInvalidate = false;
//
//                    if(!pointsList.get(pointerId).isEmpty()){
//                        int size = pointsList.get(pointerId).size();
//                        PointF lastP = pointsList.get(pointerId).get(size - 1);
//                        int dist = (Props.getMainboardVersion() == Props.Mainboard.V_828 ? 3 : 1);
////                    if (Math.abs(lastP.x - x) <= 1 && Math.abs(lastP.y - y) <= 1) {
//                        if (Math.abs(lastP.x - x) <= dist && Math.abs(lastP.y - y) <= dist) {
//                            break;
//                        }
//                    }
//                    pointsList.get(pointerId).add(new PointF(x, y));
//                } else {
//                    final BaseItem activeItem = activeItems.get(pointerId);
                if (activeItems.get(pointerId) instanceof Line) {
//                        Line line = (Line) activeItem;
                    ((Line) activeItems.get(pointerId)).add(x, y);
//                        Log.d("getpoints", "Add(id:"+pointerId+"): "+(int)x +", "+(int)y);
                }
//                }
                break;
            }
            case SELECT: {
                if (selectionMode != SelectionMode.NONE) {
                    if (!Props.getFlavorChipset().equals(Props.Flavor.Chipset.Amlogic))
                        SystemPropertiesUtils.setValue(SystemPropertiesUtils.KEY_FRAME_BUFFER_LOCK, LOCK_FRAME_BUFFER_OFF);
                }
                switch (selectionMode) {
                    case NONE: {
                        if (selectionPointerId == pointerId) {
                            selectionLine.add(x, y);
                        }
                        break;
                    }
                    case MOVE: {
                        if (fingerActiveCount == 1) {
                            final PointF lastPoint = lastDragPoints[pointerId];
                            final float dx = x - lastPoint.x;
                            final float dy = y - lastPoint.y;
                            transform.moveBy(dx, dy);
                            selectionBox.moveBy(dx, dy);
                        }
                        break;
                    }
                    case ROTATE: {
                        if (fingerActiveCount == 1) {
                            final PointF rotatePoint = selectionBox.getRotatePoint();
                            final float angle = getAngle(rotatePoint.x, rotatePoint.y, x, y);
                            selectionBox.setRotation(angle - rotateAngleOffset);
                        }
                        break;
                    }
                    case ROTATE_AND_SCALE: {
                        if (fingerActiveCount == 2) {
                            float oldWidth = downPoints[0].x - downPoints[1].x;
                            float oldHeight = downPoints[0].y - downPoints[1].y;
                            float newWidth = dragPoints[0].x - dragPoints[1].x;
                            float newHeight = dragPoints[0].y - dragPoints[1].y;
                            oldLength.set(oldWidth, oldHeight);
                            newLength.set(newWidth, newHeight);
                            float rate = newLength.length() / oldLength.length();

                            final float angle = getAngle(dragPoints[0].x, dragPoints[0].y, dragPoints[1].x, dragPoints[1].y);

                            selectionBox.setRotation(angle - rotateAngleOffset);

                            float scaledSelectionWidth = rate * selectionBox.getWidth();
                            float scaledSelectionHeight = rate * selectionBox.getHeight();

                            // limit scaling
                            if (scaledSelectionHeight < minItemSize) {
                                rate = minItemSize / selectionBox.getHeight();
                            } else if (scaledSelectionWidth < minItemSize) {
                                rate = minItemSize / selectionBox.getWidth();
                            } else if (scaledSelectionHeight > height) {
                                rate = height / selectionBox.getHeight();
                            } else if (scaledSelectionWidth > width) {
                                rate = width / selectionBox.getWidth();
                            }

                            float tempW = selectionBox.getDownScaleWidthLimit();
                            float tempH = selectionBox.getDownScaleHeightLimit();
                            log("tempW:" + tempW + ",tempH:" + tempH);
                            if (tempW > tempH) {
                                if (rate < tempW) {
                                    rate = tempW;
                                }
                            } else {
                                if (rate < tempH) {
                                    rate = tempH;
                                }
                            }
                            log("rate in drag:" + rate);

                            selectionBox.setScale(rate, rate);
                        }
                        break;
                    }
                    case REDIRECT: {
                        if (fingerActiveCount == 1) {
                            float oldWidth = selectionBox.getScalePoint().x - downPoints[0].x;
                            float oldHeight = selectionBox.getScalePoint().y - downPoints[0].y;
                            float newWidth = selectionBox.getScalePoint().x - dragPoints[0].x;
                            float newHeight = selectionBox.getScalePoint().y - dragPoints[0].y;
                            oldLength.set(oldWidth, oldHeight);
                            newLength.set(newWidth, newHeight);
                            float rate = newLength.length() / oldLength.length();

                            final float angle = getAngle(selectionBox.getScalePoint().x, selectionBox.getScalePoint().y, dragPoints[0].x, dragPoints[0].y);


                            selectionBox.setRotation(angle - rotateAngleOffset);

                            float scaledSelectionWidth = rate * selectionBox.getWidth();
                            float scaledSelectionHeight = rate * selectionBox.getHeight();

                            if (scaledSelectionHeight > height) {
                                rate = height / selectionBox.getHeight();
                            } else if (scaledSelectionWidth > width) {
                                rate = width / selectionBox.getWidth();
                            }

                            selectionBox.setScale(rate, rate);
                        }
                        break;
                    }
                    case SCALE: {
                        if (fingerActiveCount == 1) {
                            final PointF scalePoint = selectionBox.getScalePoint();
                            float newWidth = dragPoints[0].x - scalePoint.x;
                            float oldWidth = downPoints[0].x - scalePoint.x;
                            float newHeight = dragPoints[0].y - scalePoint.y;
                            float oldHeight = downPoints[0].y - scalePoint.y;
                            float widthlimit = selectionBox.getDownScaleWidthLimit();
                            float heightlimit = selectionBox.getDownScaleHeightLimit();
                            boolean reflect = false;

                            float rateWidth = 1;
                            float rateHeight = 1;
                            switch (selectionControl) {
                                case SelectionBox.Control.HANDLES_LEFT_BOTTOM:
                                case SelectionBox.Control.HANDLES_LEFT_TOP:
                                case SelectionBox.Control.HANDLES_RIGHT_TOP:
                                case SelectionBox.Control.HANDLES_RIGHT_BOTTOM: {
                                    rateWidth = newWidth / oldWidth;
                                    if (rateWidth < widthlimit / 3 && rateWidth > 0) {
                                        reflect = true;
                                    }

                                    if (Math.abs(rateWidth) < Math.abs(widthlimit)) {
                                        if (rateWidth > 0) {
                                            rateWidth = widthlimit;
                                        } else {
                                            rateWidth = -widthlimit;
                                        }
                                    }
                                    if (reflect) {
                                        rateWidth = -rateWidth;
                                    }
                                    rateHeight = rateWidth;
                                    break;
                                }
                                case SelectionBox.Control.HANDLES_LEFT:
                                case SelectionBox.Control.HANDLES_RIGHT: {
                                    rateWidth = newWidth / oldWidth;
                                    if (rateWidth < widthlimit / 3 && rateWidth > 0) {
                                        reflect = true;
                                    }

                                    if (Math.abs(rateWidth) < Math.abs(widthlimit)) {
                                        if (rateWidth > 0) {
                                            rateWidth = widthlimit;
                                        } else {
                                            rateWidth = -widthlimit;
                                        }
                                    }
                                    if (reflect) {
                                        rateWidth = -rateWidth;
                                    }
                                    rateHeight = 1;
                                    break;
                                }
                                case SelectionBox.Control.HANDLES_TOP:
                                case SelectionBox.Control.HANDLES_BOTTOM: {
                                    rateHeight = newHeight / oldHeight;
                                    if (rateHeight < heightlimit / 3 && rateHeight > 0) {
                                        reflect = true;
                                    }

                                    if (Math.abs(rateHeight) < Math.abs(heightlimit)) {
                                        if (rateHeight > 0) {
                                            rateHeight = heightlimit;
                                        } else {
                                            rateHeight = -heightlimit;
                                        }
                                    }
                                    if (reflect) {
                                        rateHeight = -rateHeight;
                                    }
                                    rateWidth = 1;
                                    break;
                                }
                            }

                            /*
                            float scaledSelectionWidth = rateWidth * selectionBox.getWidth();
                            float scaledSelectionHeight = rateHeight * selectionBox.getHeight();

                            // limit scaling
                            if (scaledSelectionHeight < minItemSize) {
                                rateHeight = minItemSize / selectionBox.getHeight();
                            } else if (scaledSelectionWidth < minItemSize) {
                                rateWidth = minItemSize / selectionBox.getWidth();
                            } else if (scaledSelectionHeight > height) {
                                rateHeight = height / selectionBox.getHeight();
                            } else if (scaledSelectionWidth > width) {
                                rateWidth = width / selectionBox.getWidth();
                            }
                            */

                            selectionBox.setScale(rateWidth, rateHeight);
                        }
                        break;
                    }
                    case TRIMMING: {
                        if (fingerActiveCount == 1) {
                            selectionBox.draggingTrimmingCorner(x, y);
//                            selectionBox.checkTrimmingRect();
                        }
                        break;
                    }
                    case TEXT_SCALE: {
                        Word activeWord = null;
                        if (selectedItems.size() == 1 && selectedItems.get(0) instanceof Word) {
                            activeWord = (Word) selectedItems.get(0);
                        }
                        if (activeWord != null) {
//                            if (Math.abs(lastDragPoints[pointer].x - x) < 1 && Math.abs(lastDragPoints[pointer].y - y) < 1) {
//                                break;
//                            }
                            activeWord.updateScalingTextBoxPreview(x, y);
                        }
                        break;
                    }
                    case DISABLE: {
                        break;
                    }
                }

                break;
            }
            case TEXT: {
                if (pointerId != 0) {
                    break;
                }
                switch (selectionMode) {
                    case TEXT_SCALE: {
                        if (activeWord != null) {
//                            if (Math.abs(lastDragPoints[pointer].x - x) < 1 && Math.abs(lastDragPoints[pointer].y - y) < 1) {
//                                break;
//                            }
                            activeWord.updateScalingTextBoxPreview(x, y);
                        }
                        break;
                    }
                    case MOVE: {
                        final PointF lastPoint = lastDragPoints[pointerId];
                        final float dx = x - lastPoint.x;
                        final float dy = y - lastPoint.y;
                        transform.moveBy(dx, dy);
                        selectionBox.moveBy(dx, dy);
                        break;
                    }
                    case ROTATE: {
                        final PointF rotatePoint = selectionBox.getRotatePoint();
                        final float angle = getAngle(rotatePoint.x, rotatePoint.y, x, y);
                        selectionBox.setRotation(angle - rotateAngleOffset);
                        break;
                    }
                    case ROTATE_AND_SCALE: {
                        break;
                    }
                    case SCALE: {
                        final PointF scalePoint = selectionBox.getScalePoint();
                        float newWidth = Math.abs(dragPoints[0].x - scalePoint.x);
//                        float newHeight = Math.abs(dragPoints[0].y - scalePoint.y);
                        float oldWidth = Math.abs(downPoints[0].x - scalePoint.x);
//                        float oldHeight = Math.abs(downPoints[0].y - scalePoint.y);
                        float rateX = newWidth / oldWidth;
//                        float rateY = newHeight / oldHeight;

                        // take rate X as scale rate
                        selectionBox.setScale(rateX, rateX);
                        break;
                    }
                }
                break;
            }


        }

        lastDragPoints[pointerId].set(dragPoints[pointerId]);

        if (requestInvalidate) {
            invalidateRender();
        }
    }

    /**
     * Called when a touch up event occurs.
     */
    private void onTouchUp(int pointerId, float x, float y) {
        isscale = false;
        log("onTouchUp: pointerId=" + pointerId + ", x=" + x + ", y=" + y + ", device:" + getDevice(pointerId)
                + ", selected items size : " + selectedItems.size() + ", state items size : " + state.getItems().size()
                + ", isMultiTouchAllowed:" + isMultiTouchAllowed + ", isMultiFingerActived:" + isMultiFingerActive);
        boolean flag = true;
        if (state == null) {
            return;
        }
        x += scrollX;
        y += scrollY;

        fingerActiveCount--;
        Input input = pointerInputs[pointerId];
//        Input input = getInputForPointer(pointerId);

//        if (!isMultiTouchAllowed && pointerId != 0) {
////            if ( fingerActiveCount == 0)
////                isMultiTouchAllowed = true;
//            log("return cause disabled multi touch.");
//            return;
//        }
//        log("- input=" + input);
        if (input == null) {
//            log("input null on touch up. finger count :" + fingerActiveCount);
            if (fingerActiveCount == 0) {
                for (int i = 0; i < MAX_INPUT_POINTERS; i++) {
                    if (pointerInputs[i] != null) {
                        fingerActiveCount++;
                        onTouchUp(i, dragPoints[i].x, dragPoints[i].y);
                    }
                }
                for (int i = 0, size = activeItems.size(); i < size; i++) {
                    final BaseItem item = activeItems.get(i);
                    if (item instanceof Line) {
                        cacheQueue.add(item);
                        state.addItem(item);

                        Action action = Action.createInsertAction(item.getId());
                        state.addAction(action);
                    }
                }

                if (activeItems.size() > 0) {
                    activeItems.clear();
                    renderCached();
                }


                multiFingerCount = 0;
                if (objectEraserPen != null) {
                    objectEraserPen.clear();
                    objectEraserPen = null;
                    listener.setMainThreadEraserAreaVisible(false, eraserSize, input);
                }
                if (partialEraserPen != null) {
                    doPartialEraser(partialEraserPen);
                }


                if (pointerId < 5)
                    pointsArray[pointerId].set(0, 0);

                resetTouch();
            }
            return;
        }
        if (!isInputAllowed(pointerId, input)) {
            return;
        }

        if (getDevice(pointerId) != Device.PALM) {
            dragPoints[pointerId].set(0, 0);
        }

        if (fingerActiveCount < 0) {
            log("fingerCounter < 0");
        }

        // find new eraser pointer
        if (eraserPointerId != INVALID_POINTER_ID) {
            eraserPointerId = INVALID_POINTER_ID;

            for (int i = 0; i < pointerInputs.length; i++) {
                if (i != pointerId && pointerInputs[i] == Input.OBJECT_ERASER) {
                    eraserPointerId = i;
                    break;
                }
            }
        }

        boolean refreshCached = false;

        switch (input) {
            case AUTOSHAPE: {
                final BaseItem activeItem = activeItems.get(pointerId);
                final Autoshape temp = (Autoshape) activeItem;
                log("pointerid:" + pointerId + ", activeItem:" + (activeItem != null) + ", isDistanceAcceptable:" + (temp.isDistanceAcceptable()));
                if (activeItem != null) {
                    if (!temp.isDistanceAcceptable()) {
                        temp.defaultShapeSet(x, y);
                    }
                    cacheQueue.add(activeItem);
                    state.addItem(activeItem);
                    Action action = Action.createInsertAction(activeItem.getId());
                    state.addAction(action);
                    refreshCached = true;
                }
                break;
            }
            case MOVE_CANVAS: {
                if (state.isAnnotationState()) {
                    break;
                }

                if (fingerActiveCount < 2) {
                    showHintText = false;
                    showHintStickynote = false;
                }

                if (fingerActiveCount <= 0) {
                    isMultiFingerActive = false;
                    multiFingerCount = -1;
                    scrollPointerId = INVALID_POINTER_ID;
                }

                // scroll grid
                scrollGridStartX += scrollX;
                scrollGridStartY += scrollY;
                appliedScale = bgzoomScales[MainActivity.stateIndex];

                // reset scrolling
                transform.clear();
                transform.setTranslate(scrollX, scrollY);
                transform.setScalePoint(zoomPoint.x, zoomPoint.y);
                transform.setScale(zoomScale, zoomScale);
                transform.apply(state.getItems());
                state.updateUndoActionTransform(scrollX, scrollY, zoomScale, zoomPoint);
                state.updateRedoActionTransform(scrollX, scrollY, zoomScale, zoomPoint);

                transform.clear();
                zoomScale = 1;
                scrollX = 0;
                scrollY = 0;
                first = false;

                justTouchUp = true;

                zDownPoints[pointerId].set(0, 0);
                zDragPoints[pointerId].set(0, 0);
                zLastDragPoints[pointerId].set(0, 0);

                refreshCached = true;

                if (state != null) {
                    state.setAppliedScale(appliedScale);
                }

                break;
            }
            case OBJECT_ERASER: {
                if (fingerActiveCount == 0) {
                    if (objectEraserPen != null) {
                        objectEraserPen.add(x, y);
                        List<Integer> deletedList = new ArrayList<>();
                        List<BaseItem> deletedItems = new ArrayList<>();
                        List<Integer> sizeList = new ArrayList<>();
                        if (FastDraw.isSupportedCanvasFastDrawEraser()) {
                            objectEraserPen.updateAll();
                        }
                        for (int ii = state.getItems().size() - 1; ii >= 0; ii--) {
                            BaseItem baseItem = state.getItems().get(ii);
                            if (baseItem instanceof Line) {
                                if (checkIntersect((Line) baseItem, objectEraserPen)) {
                                    state.getItems().remove(baseItem);
                                    deletedList.add(ii);
                                    deletedItems.add(baseItem);
                                    sizeList.add(0);
                                }
                            }
                        }

                        addEraserAction(deletedItems, deletedList, sizeList);

                    }
//                    eraserPointerId = INVALID_POINTER_ID;
//                    eraserArea.setVisible(false);
                    refreshCached = true;
                    if (objectEraserPen != null)
                        objectEraserPen.clear();
                    objectEraserPen = null;
                    if (listener != null) {
                        listener.setMainThreadEraserAreaVisible(false, 0, input);
                    }
                    resetTouch();

                }
                break;
            }
            case PARTIAL_ERASER: {
                if (!FastDraw.isSupportedCanvasFastDrawEraser()) {
                    if (fingerActiveCount == 0)
                        eraserSize = getEraserSize(getDevice(pointerId));
                } else {
                    if (fingerActiveCount == 0 && partialEraserPen != null) {

                        partialEraserPen.add(x, y);
                        List<BaseItem> erasedList = new ArrayList<>();

                        cached.eraseColor(Color.TRANSPARENT);

                        List<Integer> deletedList = new ArrayList<>();
                        List<Integer> sizeList = new ArrayList<>();
                        List<BaseItem> deletedItems = new ArrayList<>();

                        partialEraserPen.updateAll();

                        for (int ii = state.getItems().size() - 1; ii >= 0; ii--) {
                            BaseItem baseItem = state.getItems().get(ii);

                            if (baseItem instanceof Line) {
                                if (checkIntersect((Line) baseItem, partialEraserPen)) {
                                    List<BaseItem> temperasedList;
                                    temperasedList = ((Line) baseItem).splitLine(partialEraserPen);
                                    state.getItems().remove(baseItem);
                                    if (temperasedList.size() > 0) {
                                        erasedList.addAll(temperasedList);
                                    }

                                    if (erasedList.size() > 0) {
                                        for (int i = 0; i < erasedList.size(); i++) {
                                            if ((ii + i) >= state.getItems().size()) {
                                                state.getItems().add(erasedList.get(i));
                                            } else {
                                                state.getItems().add(ii + i, erasedList.get(i));
                                            }
                                        }
                                    }

                                    deletedList.add(ii);
                                    sizeList.add(erasedList.size());
                                    deletedItems.add(baseItem);

                                    erasedList.clear();
                                }
                            }
                        }

                        addEraserAction(deletedItems, deletedList, sizeList);
                        refreshCached = true;

                        if (partialEraserPen != null) {
                            partialEraserPen.clear();
                            partialEraserPen = null;
                        }
                        if (listener != null) {
                            listener.setMainThreadEraserAreaVisible(false, eraserSize, input);
                        }
                        notifyEraserDone = true;
                    }
                }
                break;
            }
            case PEN:
            case HIGHLIGHTER: {
                final BaseItem activeItem = activeItems.get(pointerId);
                log("activeItem null?" + (activeItem == null));
                if (activeItem != null) {
                    if (activeItem instanceof Line) {
                        Line line = (Line) activeItem;
                        line.add(x, y);
                    }
                    cacheQueue.add(activeItem);
                    state.addItem(activeItem);

                    Action action = Action.createInsertAction(activeItem.getId());
                    state.addAction(action);

                    refreshCached = true;
                }
                break;
            }
            case SELECT: {
                if (!Props.getFlavorChipset().equals(Props.Flavor.Chipset.Amlogic))
                    SystemPropertiesUtils.setValue(SystemPropertiesUtils.KEY_FRAME_BUFFER_LOCK, LOCK_FRAME_BUFFER_OFF);
                if (Props.getMainboardVersion() != Props.Mainboard.V_811) {//处理残影，但会导致闪屏
                    cleanFrameBufferCount = 0;
                    invalidateRender();
                }
                if (selectionMode != SelectionMode.NONE) {
                    state.setPreviewDirty(true);
                    resetAutomaticPreviewUpdate();
                }

                selectionBox.setShowSideHandles(true);

                switch (selectionMode) {
                    case NONE: {
                        boolean isLongPress = false;
                        if (listener != null) {
                            isLongPress = listener.isLongPressing();
                        }

                        if (selectionPointerId == pointerId && !isLongPress) {
                            selectionPointerId = INVALID_POINTER_ID;
                            selectionLine.add(x, y);
                            selectionLine.startAreaAnimation();
                            selectionLine.getRegion(selectRegion);

                            boolean singleSelect = selectionLine.isSingleSelect();

                            if (state != null) {
                                state.clearSelected();
                            }
                            selectedItems.clear();
                            for (int ii = state.getItems().size() - 1; ii >= 0; ii--) {
                                BaseItem baseItem = state.getItems().get(ii);
                                // op will clear data of region
                                if (checkIntersect(baseItem, selectRegion)) {
                                    baseItem.setSelected(true);
                                    selectedItems.add(baseItem);

                                    if (singleSelect) {
                                        break;
                                    }
                                }
                            }

                            if (!singleSelect && selectedItems.size() > 1) {
                                Collections.reverse(selectedItems);
                            }

                            if (!selectedItems.isEmpty()) {
                                updateSelectionBox();
                                renderSelectionPreview();
                                selectionBox.setOutlineVisible(true);
                            }

                            refreshCached = true;
                        }
                        break;
                    }

                    case MOVE: {
                        log("in move touchup.");
                        if (fingerActiveCount == 0 && getDevice(0) != Device.PALM) {
                            log("in move touch up if:");
                            final PointF lastPoint = lastDragPoints[pointerId];
                            final float dx = x - lastPoint.x;
                            final float dy = y - lastPoint.y;

                            transform.moveBy(dx, dy);
                            selectionBox.moveBy(dx, dy);

                            transform.apply(selectedItems);

//                            if (dx!=0.0f || dy!=0.0f) {
                            if (transform.getTranslateX() != 0.0f || transform.getTranslateY() != 0.0f) {
                                long[] movedId = new long[selectedItems.size()];
                                for (int i = 0; i < selectedItems.size(); i++) {
                                    movedId[i] = selectedItems.get(i).getId();
                                }
                                if (movedId.length > 0) {
                                    Action action = Action.createMoveAction(movedId, transform.getTranslateX(), transform.getTranslateY());
                                    state.addAction(action);
                                    renderCached();
                                }
                            }

                            transform.clear();

                            selectionBox.setPreviewVisible(false);
                            selectionBox.setOutlineVisible(true);
                            if (listener != null) {
                                listener.onSelectionBoxBoundsChange(selectionBox.getLeft(),
                                        selectionBox.getTop(), selectionBox.getRight(),
                                        selectionBox.getBottom());
                                listener.onSelectionBoxVisibleChange(true, selectionBox.getSelectionRenderMode());
                            }
                            selectionMode = SelectionMode.DISABLE;

                            if (selectionBox.getHeadTailVisible()) {
                                updateHeadTailButton(selectionBox, ((Autoshape) selectedItems.get(0)).getPoints());
                            }

                        }
                        break;
                    }
                    case ROTATE: {
                        if (fingerActiveCount == 0 && getDevice(0) != Device.PALM) {
                            final PointF rotatePoint = selectionBox.getRotatePoint();
                            final float degrees = selectionBox.getRotation();
                            transform.setRotatePoint(rotatePoint.x, rotatePoint.y);
                            transform.setRotate(degrees);

                            transform.apply(selectedItems);

                            if (transform.getRotate() != 0.0f) {
                                long[] rotatedId = new long[selectedItems.size()];
                                for (int i = 0; i < selectedItems.size(); i++) {
                                    rotatedId[i] = selectedItems.get(i).getId();
                                }
                                if (rotatedId.length > 0) {
                                    Action action = Action.createRotateAction(rotatedId, transform.getRotatePoint(),
                                            transform.getRotate());
                                    state.addAction(action);
                                    renderCached();
                                }
                            }

                            transform.clear();

                            updateSelectionBox();
                            if (selectionBox.getHeadTailVisible()) {
                                updateHeadTailButton(selectionBox, ((Autoshape) selectedItems.get(0)).getPoints());
                            }
                            renderSelectionPreview();
                            selectionBox.setPreviewVisible(false);

                            selectionBox.setOutlineVisible(true);
                            if (listener != null) {
                                listener.onSelectionBoxVisibleChange(true, selectionBox.getSelectionRenderMode());
                            }
                            selectionMode = SelectionMode.DISABLE;
                        }
                        break;
                    }
                    case ROTATE_AND_SCALE: {
                        if (fingerActiveCount < 2) {
                            selectionMode = SelectionMode.DISABLE;
                            // calculate mid point difference for move difference
//                            midPointStart.set((downPoints[0].x + downPoints[1].x) / 2, (downPoints[0].y + downPoints[1].y) / 2);
                            // calculate rotate degree and scale rate
                            float oldWidth = downPoints[0].x - downPoints[1].x;
                            float oldHeight = downPoints[0].y - downPoints[1].y;
                            float newWidth = lastDragPoints[0].x - lastDragPoints[1].x;
                            float newHeight = lastDragPoints[0].y - lastDragPoints[1].y;
                            oldLength.set(oldWidth, oldHeight);
                            newLength.set(newWidth, newHeight);
                            float rate = selectionBox.getScaleX();

                            log("rate in up:" + rate);
                            float degree = selectionBox.getRotation();

                            // scale and rotation use the same pivot point
                            final PointF rotationPoint = selectionBox.getRotatePoint();
                            final float centerX = rotationPoint.x;
                            final float centerY = rotationPoint.y;

                            if (degree != 0.0f || (rate != 1 && rate != 0)) {
                                long[] id = new long[selectedItems.size()];
                                for (int i = 0; i < selectedItems.size(); i++) {
                                    BaseItem item = selectedItems.get(i);
                                    id[i] = item.getId();
                                    if (item instanceof Word) {
                                        ((Word) item).saveWordState();
                                    }
                                }
                                if (id.length > 0) {
                                    // use newLength to pass center point to Action
                                    newLength.set(centerX, centerY);
                                    Action newAction = Action.createRoscaleAction(id, tempPointF,
                                            degree, rate, rate, newLength);
                                    state.addAction(newAction);
                                    renderCached();
                                }
                            }

                            if (rate != 0) {
                                transform.setScalePoint(centerX, centerY);
                                transform.setScale(rate, rate);
                            }

                            transform.setRotatePoint(centerX, centerY);
                            transform.setRotate(degree);

                            transform.apply(selectedItems);
                            transform.clear();

                            updateSelectionBox();
                            if (selectionBox.getHeadTailVisible()) {
                                updateHeadTailButton(selectionBox, ((Autoshape) selectedItems.get(0)).getPoints());
                            }
                            renderSelectionPreview();
                            selectionBox.setPreviewVisible(false);

                            selectionBox.setOutlineVisible(true);
                            if (listener != null) {
                                listener.onSelectionBoxVisibleChange(true, selectionBox.getSelectionRenderMode());
                            }
                        }
                        break;
                    }
                    case REDIRECT: {
                        if (fingerActiveCount == 0) {
                            float oldWidth = selectionBox.getScalePoint().x - downPoints[0].x;
                            float oldHeight = selectionBox.getScalePoint().y - downPoints[0].y;
                            float newWidth = selectionBox.getScalePoint().x - dragPoints[0].x;
                            float newHeight = selectionBox.getScalePoint().y - dragPoints[0].y;
                            oldLength.set(oldWidth, oldHeight);
                            newLength.set(newWidth, newHeight);
//                            float rate = newLength.length() / oldLength.length();

                            float rate = selectionBox.getScaleX();

                            float degree = selectionBox.getRotation();

                            // scale and rotation use the same pivot point
                            final PointF rotationPoint = selectionBox.getScalePoint();
                            final float centerX = rotationPoint.x;
                            final float centerY = rotationPoint.y;

                            if (degree != 0.0f || (rate != 1 && rate != 0)) {
                                long[] id = new long[selectedItems.size()];
                                for (int i = 0; i < selectedItems.size(); i++) {
                                    BaseItem item = selectedItems.get(i);
                                    id[i] = item.getId();
                                    if (item instanceof Word) {
                                        ((Word) item).saveWordState();
                                    }
                                }
                                if (id.length > 0) {
                                    // use newLength to pass center point to Action
                                    newLength.set(centerX, centerY);
                                    Action newAction = Action.createRoscaleAction(id, tempPointF,
                                            degree, rate, rate, newLength);
                                    state.addAction(newAction);
                                    renderCached();
                                }
                            }

                            if (rate != 0) {
                                transform.setScalePoint(centerX, centerY);
                                transform.setScale(rate, rate);
                            }

                            transform.setRotatePoint(centerX, centerY);
                            transform.setRotate(degree);

                            transform.apply(selectedItems);
                            transform.clear();

                            updateSelectionBox();
                            if (selectionBox.getHeadTailVisible()) {
                                updateHeadTailButton(selectionBox, ((Autoshape) selectedItems.get(0)).getPoints());
                            }
                            renderSelectionPreview();
                            selectionBox.setPreviewVisible(false);

                            selectionBox.setOutlineVisible(true);
                            if (listener != null) {
                                listener.onSelectionBoxVisibleChange(true, selectionBox.getSelectionRenderMode());
                            }
                        }
                        break;
                    }
                    case SCALE: {
                        if (fingerActiveCount == 0) {
                            final PointF scalePoint = selectionBox.getScalePoint();
                            float scaleX = selectionBox.getScaleX();
                            float scaleY = selectionBox.getScaleY();

                            float scaledSelectionWidth = Math.abs(scaleX * selectionBox.getWidth());
                            float scaledSelectionHeight = Math.abs(scaleY * selectionBox.getHeight());

                            // limit scaling
                            if (scaledSelectionHeight < minItemSize) {
                                scaleY = minItemSize / selectionBox.getHeight() * (scaleY < 0 ? -1 : 1);
                            } else if (scaledSelectionWidth < minItemSize) {
                                scaleX = minItemSize / selectionBox.getWidth() * (scaleX < 0 ? -1 : 1);
                            } else if (scaledSelectionHeight > height) {
                                scaleY = height / selectionBox.getHeight() * (scaleY < 0 ? -1 : 1);
                            } else if (scaledSelectionWidth > width) {
                                scaleX = width / selectionBox.getWidth() * (scaleX < 0 ? -1 : 1);
                            }

                            transform.setScalePoint(scalePoint.x, scalePoint.y);
                            transform.setScale(scaleX, scaleY);

                            transform.apply(selectedItems);

                            if (transform.getScaleX() != 0 && transform.getScaleY() != 0) {
                                List<Long> scaledId = new ArrayList<>();
                                for (int i = 0; i < selectedItems.size(); i++) {
                                    BaseItem item = selectedItems.get(i);
                                    if (item instanceof Word) {
                                        Word w = (Word) item;
                                        if (w.isTextBoxBoundsChanged()) {
                                            w.saveWordState();
                                            scaledId.add(w.getId());
                                        }
                                    } else {
                                        scaledId.add(item.getId());
                                    }
                                }

                                if (scaledId.size() > 0) {
                                    long[] scaledIdArray = new long[scaledId.size()];
                                    for (int i = 0; i < scaledId.size(); i++) {
                                        scaledIdArray[i] = scaledId.get(i);
                                    }
                                    Action action = Action.createScaleAction(scaledIdArray, transform.getScalePoint(), transform.getScaleX(), transform.getScaleY());
                                    state.addAction(action);
                                    renderCached();
                                }
                            }

                            transform.clear();

                            updateSelectionBox();
                            if (selectionBox.getHeadTailVisible()) {
                                updateHeadTailButton(selectionBox, ((Autoshape) selectedItems.get(0)).getPoints());
                            }
                            selectionBox.setPreviewVisible(false);
                            selectionBox.setOutlineVisible(true);
                            renderSelectionPreview();

                            if (listener != null) {
                                listener.onSelectionBoxVisibleChange(true, selectionBox.getSelectionRenderMode());
                            }
                        }
                        break;
                    }
                    case TRIMMING: {
                        selectionLine.startAreaAnimation();
                        // not necessary for normal photo, not tested for tiny photo
//                        selectionBox.checkTrimmingRect();
                        return;
//                        break;
                    }
                    case TEXT_SCALE: {
                        if (pointerId != 0) {
                            break;
                        }
                        boolean textScaleValid = selectedItems.size() == 1 && selectedItems.get(0) instanceof Word;
                        Word activeWord = null;
                        if (textScaleValid) {
                            activeWord = (Word) selectedItems.get(0);
                        } else {
                            break;
                        }
                        if (activeWord == null || !activeWord.getTextStatus().equals(Word.TextStatus.SELECTED)) {
                            log("Error when text scale, activeWord is null? " + (activeWord == null) +
                                    ", text status : " + (activeWord == null ? "NONE" : activeWord.getTextStatus()));
                            break;
                        }

                        activeWord.textScaleFromPoint(x, y);

                        if (activeWord.isTextBoxBoundsChanged()) {
                            activeWord.saveWordState();

                            Action action = Action.createTextScaleAction(activeWord.getId());
                            state.addAction(action);
                            renderCached();
                        }

                        activeWord.setTextScaling(false);
                        updateSelectionBox();
                        selectionBox.updateScaleButtonBound(activeWord.getTextBoxBounds(), activeWord.getRotation());
                        selectionBox.setPreviewVisible(false);
                        selectionBox.setOutlineVisible(true);
                        renderSelectionPreview();
                        if (listener != null) {
//                            listener.onSelectionBoxVisibleChange(true, selectionBox.isStickyNoteSelected());
                            listener.onSelectionBoxVisibleChange(true, selectionBox.getSelectionRenderMode());
                        }

                        refreshCached = true;
                        break;
                    }
                    case DISABLE: {
                        // do nothing
                        break;
                    }
                }


                // reset mode
                if (fingerActiveCount == 0) {
                    selectionMode = SelectionMode.NONE;
                    // uncommenting this line causes selection area to not show
//                    selectionLine.setVisible(false);
                    selectionBox.setImageTrimming(false);
                    isTrimmingActive = false;
                    log("mode reset.");

                    log("fingerCount at mid:" + fingerActiveCount);
//                    suspendTouch = false;

                    if (!selectedItems.isEmpty()) {
                        updateSelectionBox();
                        if (selectionBox.getHeadTailVisible()) {
                            updateHeadTailButton(selectionBox, ((Autoshape) selectedItems.get(0)).getPoints());
                        }
                        renderSelectionPreview();
                        selectionBox.setOutlineVisible(true);
                        if (listener != null) {
                            listener.onSelectionBoxVisibleChange(true, selectionBox.getSelectionRenderMode());
                        }
                    }

                    refreshCached = true;

                }
                break;
            }
            case TEXT: {
                if (pointerId != 0) {
                    break;
                }

                if (selectionMode != SelectionMode.NONE && selectedItems.size() <= 0) {
                    break;
                }

                selectionBox.setShowSideHandles(false);

                selectionBox.setSelectionRenderMode(SelectionBox.ButtonMode.NORMAL);

                switch (selectionMode) {
                    case TEXT_SCALE: {
                        if (pointerId != 0) {
                            break;
                        }
                        if (activeWord == null || !activeWord.getTextStatus().equals(Word.TextStatus.SELECTED)) {
                            log("Error when text scale, activeWord is null? " + (activeWord == null) +
                                    ", text status : " + (activeWord == null ? "NONE" : activeWord.getTextStatus()));
                            break;
                        }

                        activeWord.textScaleFromPoint(x, y);

                        if (activeWord.isTextBoxBoundsChanged()) {
                            activeWord.saveWordState();

                            Action action = Action.createTextScaleAction(activeWord.getId());
                            state.addAction(action);
                            renderCached();
                        }

                        activeWord.setTextScaling(false);
                        updateSelectionBox();
                        selectionBox.updateScaleButtonBound(activeWord.getTextBoxBounds(), activeWord.getRotation());
                        selectionBox.setPreviewVisible(false);
                        selectionBox.setOutlineVisible(true);
                        renderSelectionPreview();
                        if (listener != null) {
//                            listener.onSelectionBoxVisibleChange(true, selectionBox.isStickyNoteSelected());
                            listener.onSelectionBoxVisibleChange(true, selectionBox.getSelectionRenderMode());
                        }

                        refreshCached = true;
                        break;
                    }

                    case NONE: {
                        selectedItems.clear();

                        if (activeWord != null && activeWord.getTextStatus().equals(Word.TextStatus.SELECTED)) {
                            selectedItems.add(activeWord);
                            updateSelectionBox();
//                            selectionBox.updateScaleButtonBound(activeWord.getTextBoxBounds(), activeWord.getRotation());
                            renderSelectionPreview();
                            selectionBox.setOutlineVisible(true);
                            if (listener != null) {
                                listener.onSelectionBoxVisibleChange(true, selectionBox.getSelectionRenderMode());

                                listener.onSetActiveWord(false, null);
                            }
                        }

                        refreshCached = true;
                        break;
                    }
                    case MOVE: {
                        final PointF lastPoint = lastDragPoints[pointerId];
                        final float dx = x - lastPoint.x;
                        final float dy = y - lastPoint.y;
                        transform.moveBy(dx, dy);
                        selectionBox.moveBy(dx, dy);
                        transform.apply(selectedItems);

                        if (transform.getTranslateX() != 0.0f || transform.getTranslateY() != 0.0f) {
                            long[] movedId = new long[]{selectedItems.get(0).getId()};
                            Action action = Action.createMoveAction(movedId, transform.getTranslateX(), transform.getTranslateY());
                            state.addAction(action);
                            renderCached();
                        }

                        transform.clear();
                        selectionBox.setPreviewVisible(false);
                        selectionBox.setOutlineVisible(true);
                        if (listener != null) {
                            listener.onSelectionBoxBoundsChange(selectionBox.getLeft(),
                                    selectionBox.getTop(), selectionBox.getRight(),
                                    selectionBox.getBottom());
                            selectionBox.updateScaleButtonBound(activeWord.getTextBoxBounds(), activeWord.getRotation());
                            listener.onSelectionBoxVisibleChange(true, selectionBox.getSelectionRenderMode());
                        }
                        break;
                    }
                    case ROTATE: {
                        final PointF rotatePoint = selectionBox.getRotatePoint();
                        final float degrees = selectionBox.getRotation();
                        transform.setRotatePoint(rotatePoint.x, rotatePoint.y);
                        transform.setRotate(degrees);

                        transform.apply(selectedItems);

                        if (transform.getRotate() != 0.0f) {
                            long[] rotatedId = new long[]{selectedItems.get(0).getId()};
                            Action action = Action.createRotateAction(rotatedId, transform.getRotatePoint(), transform.getRotate());
                            state.addAction(action);
                            renderCached();
                        }

                        transform.clear();
                        selectionBox.setSelectionRenderMode(SelectionBox.ButtonMode.NORMAL);
                        updateSelectionBox();
                        selectionBox.updateScaleButtonBound(activeWord.getTextBoxBounds(), activeWord.getRotation());
                        selectionBox.setPreviewVisible(false);
                        selectionBox.setOutlineVisible(true);
                        renderSelectionPreview();

                        if (listener != null) {
                            listener.onSelectionBoxVisibleChange(true, selectionBox.getSelectionRenderMode());
                        }
                        break;
                    }
                    case ROTATE_AND_SCALE: {
                        break;
                    }
                    case SCALE: {
                        if (pointerId == 0 && activeWord != null) {
                            final PointF scalePoint = selectionBox.getScalePoint();
                            final float scaleX = selectionBox.getScaleX();
                            final float scaleY = selectionBox.getScaleY();

                            transform.setScalePoint(scalePoint.x, scalePoint.y);
//                            transform.setScale(scaleX, scaleY);
                            transform.setScale(scaleX, scaleX);

                            transform.apply(selectedItems);

//                            if (transform.getScaleX()!=1.0f || transform.getScaleY()!=1.0f) {
                            if (!selectedItems.isEmpty() && activeWord.isTextBoxBoundsChanged()) {

                                activeWord.saveWordState();
                                long[] scaledId = new long[]{selectedItems.get(0).getId()};
//                                Action action = Action.createScaleAction(scaledId, transform.getScalePoint(), transform.getScaleX(), transform.getScaleY());
                                Action action = Action.createScaleAction(scaledId, transform.getScalePoint(), transform.getScaleX(), transform.getScaleX());
                                state.addAction(action);
                                renderCached();
                            }

                            transform.clear();
                            updateSelectionBox();
                            selectionBox.updateScaleButtonBound(activeWord.getTextBoxBounds(), activeWord.getRotation());
                            selectionBox.setPreviewVisible(false);
                            selectionBox.setOutlineVisible(true);
                            renderSelectionPreview();

                            if (listener != null) {
                                listener.onSelectionBoxVisibleChange(true, selectionBox.getSelectionRenderMode());
                            }
                        }
                        break;
                    }
                }

                // reset mode
                // when touch up, reset mode even if the number of active points is not 0
                selectionMode = SelectionMode.NONE;
                log("mode reset.");

                if (!selectedItems.isEmpty()) {
                    updateSelectionBox();
                    renderSelectionPreview();
                    selectionBox.setOutlineVisible(true);
                    if (activeWord != null && input == Input.TEXT) {
                        selectionBox.updateScaleButtonBound(activeWord.getTextBoxBounds(), activeWord.getRotation());
                    }
                    if (listener != null) {
                        listener.onSelectionBoxVisibleChange(true, selectionBox.getSelectionRenderMode());
                    }
                } else {
                    if (selectionBox.isVisible()) {
                        selectionBox.hide();
                        if (listener != null) {
                            listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                        }
                    }
                }
                refreshCached = true;
//                }

                break;
            }
        }
        log("fingerCount at last:" + fingerActiveCount);
        if (fingerActiveCount <= 0) {
//            suspendTouch = false;
//            log("reset suspendTouch");
//            eraserSize = ERASER_SIZE;
        }

        // autoshape check
//        for(int i = 0 ;i<state.getItems().size();i++){
//            BaseItem item = state.getItems().get(i);
//            if( item instanceof Autoshape && item.getHeight()<20 && item.getWidth() < 20 ){
//                state.removeItem(state.getItems().get(i));
//
//                refreshCached = true;
//
//                clearFrameBuffer();
//                log("clean up temp autoshape. :"+i);
//            }
//            log("autoshapecheck:"+state.getItems().size());
//        }

        if (getDevice(pointerId) == Device.PALM) {
            eraserArea.circleSet(-30, -30, 16);
            boolean otherPalmExist = false;
            for (int i = 0; i < MAX_INPUT_POINTERS; i++) {
                if (i != pointerId && getDevice(i) == Device.PALM) {
                    otherPalmExist = true;
                }
            }
            if (!otherPalmExist) {
                eraserArea.setVisible(false);
                isMultiTouchAllowed = true;
                refreshCached = true;
            }
        }

        if (eraserPointerId == INVALID_POINTER_ID) {
            eraserArea.setVisible(false);
            refreshCached = true;
        }

        if (fingerActiveCount == 0) {
            multiFingerCount = 0;
        }

        // clean up
        activeItems.put(pointerId, null);
        pointerInputs[pointerId] = null;
        selectionControl = SelectionBox.Control.NONE;

        if (fingerActiveCount == 0) {
            if (objectEraserPen != null) {
                objectEraserPen.clear();
                objectEraserPen = null;
            }
            if (partialEraserPen != null) {
                doPartialEraser(partialEraserPen);
            }
            isMultiTouchAllowed = true;

        }


        downPoints[pointerId].set(0, 0);
        dragPoints[pointerId].set(0, 0);
        if (pointerId < 5)
            pointsArray[pointerId].set(0, 0);


        if (refreshCached) {
            if (fingerActiveCount == 0) {
                state.setPreviewDirty(true);
                resetAutomaticPreviewUpdate();
            }
            renderCached();
            invalidateRender();
        }
    }

    /**
     * Returns the angle between {@code (x, y)}, {@code (centerX, centerY)}, and {@code (x, centerY)}.
     *
     * @return angle in degrees
     */
    private float getAngle(float centerX, float centerY, float x, float y) {
        return (float) Math.toDegrees(Math.atan2(-(x - centerX), (y - centerY)));
    }

    private void onInputChange(Device device, Input input) {
        final Input previousInput = inputManager.getInput(device);

        currentDevice = device;
//        if (previousInput != Input.OBJECT_ERASER) {
        if (previousInput != Input.OBJECT_ERASER && previousInput != Input.PARTIAL_ERASER && previousInput != Input.MOVE_CANVAS) {
            switch (device) {
                case FINGER: {
                    previousFingerInput = previousInput;
                    break;
                }
                case STYLUS: {
                    previousStylusInput = previousInput;
                    break;
                }
            }
        }
        // TODO : palm when move_canvas
        if (Props.getFlavorModel().equals(Props.Flavor.Model.Oem)) {
            if (input != Input.MOVE_CANVAS) {
                Device backDevice = null;
                Input backInput = null;
                if (device == Device.STYLUS) {
                    backDevice = Device.FINGER;
                    backInput = previousFingerInput == Input.MOVE_CANVAS ? Input.HIGHLIGHTER : previousFingerInput;
                } else if (device == Device.FINGER) {
                    backDevice = Device.STYLUS;
                    backInput = previousStylusInput == Input.MOVE_CANVAS ? Input.PEN : previousStylusInput;
                }

                if (backDevice != null) {
                    Input AnotherInput = inputManager.getInput(backDevice);
                    if (AnotherInput == Input.MOVE_CANVAS) {
                        inputManager.setInput(backDevice, backInput);
                    }
                }
            }
        }

        boolean updateCache = false;

        if (previousInput != input) {
            inputManager.setInput(device, input);

            if (previousInput == Input.SELECT) {
                if (state != null) {
                    state.clearSelected();
                }
                selectedItems.clear();
                selectionBox.hide();
                if (listener != null) {
                    listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                }
                updateCache = true;
            }

        }

        if (previousInput == Input.TEXT) {// If the previous input is TEXT, clear the focused/selected text box even if the the current input is TEXT.
            // The focused text box (activeWord) should be unfocused now, but it is still focused.
            // The reason why the text box is still focused is that clearTextBoxAndToggleKeyboard() is still processing (or still in input queue).
            if (activeWord != null && activeWord.getTextStatus().equals(Word.TextStatus.FOCUSED)) {
                activeWord.setTextStatus(Word.TextStatus.UNFOCUSED);
                setActiveWord(null);
            }
            if (!selectedItems.isEmpty()) {
                if (state != null) {
//                    state.clearSelected();
                    for (BaseItem item : state.getItems()) {
                        if (item instanceof Word) {
                            ((Word) item).setTextStatus(Word.TextStatus.UNFOCUSED);
                        }
                    }
                }
                selectedItems.clear();
                selectionBox.hide();
                if (listener != null) {
                    listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                }
            }
            updateCache = true;
        }

        setShowText(input == Input.TEXT && activeWord == null);
        setTextSelectionAreaVisible(input == Input.TEXT);

        if (updateCache) {
            renderCached();
            invalidateRender();
        }

        if (input == Input.PARTIAL_ERASER || input == Input.OBJECT_ERASER) {
            for (BaseItem item : state.getItems()) {
                if (item instanceof Line) {
                    ((Line) item).updateDetectRegions();
                }
            }
            inputManager.setInput(Device.PALM, input);
        }

    }

    private void sendPointerTouchUp(int pointer) {
        onTouchUp(pointer, dragPoints[pointer].x, dragPoints[pointer].y);
    }

    /**
     * called after unexpected behavior happens
     */
    private void jobsBeforeSuspend() {
        for (int i = 0; i < MAX_INPUT_POINTERS; i++) {
            if (dragPoints[i] != null && !dragPoints[i].equals(0, 0)) {
                sendPointerTouchUp(i);
            }
        }
    }


    public void setDevice(int pointer, Device device) {
        if (pointer >= 0 && pointer < pointerDevices.length) {
            pointerDevices[pointer] = device;
        }
    }

    private Device getDevice(int pointer) {
        if (pointer >= 0 && pointer < pointerDevices.length) {
            return pointerDevices[pointer];
        } else {
            return null;
        }
    }

    /**
     * Returns the input for a pointer.
     */
    private Input getInputForPointer(int pointer) {
        Device device = getDevice(pointer);
        if (device == null) {
            device = Device.FINGER;
        }
        return inputManager.getInput(device);
    }

    private boolean isInputAllowed(int pointer, Input input) {
        if (pointer >= MAX_INPUT_POINTERS) {
            log("input not allowed of overuse pointer.");
            return false;
        }

//        if(fingerActiveCount == 1 && pointer == 0 && (input == Input.SELECT||input == Input.OBJECT_ERASER||input ==Input.PARTIAL_ERASER)){
//            log("disable multi touch.");
//            isMultiTouchAllowed = false;
//        }

        if (fingerActiveCount <= 1) {
            return true;
        }
//        if(pointer > 0 && !isMultiTouchAllowed) {
//            return false;
//        }
        if (!isMultiTouchAllowed) {
            if (!(input == Input.PARTIAL_ERASER || input == Input.OBJECT_ERASER)) {
                log("return cause multi touch not allowed.");
                return false;
            }
            if (pointer > 0)
                return false;
        }
        if (isMultiFingerActive && pointer > 5) {
            return false;
        }
//        if (isMultiFingerActive && (input != Input.PARTIAL_ERASER && input != Input.MOVE_CANVAS)) {
//            log("input not allowed cause of five finger not released.");
//            return false;
//        }

        if (input == Input.SELECT) {
            if (selectionMode == SelectionMode.DISABLE) {
                log("input not allowed of disable selection mode.");
                return false;
            }
            // allow if first pointer is also in select mode and there are only two active pointers
            return pointerInputs[0] == Input.SELECT && (fingerActiveCount == 2);
//        } else if (input == Input.PEN || input == Input.HIGHLIGHTER) {
//            log("input now allowed for 0input is eraser. " + pointerInputs[0]);
//            return pointerInputs[0] != Input.OBJECT_ERASER && pointerInputs[0] != Input.PARTIAL_ERASER;
        } else {
            // allow if first pointer is not in select mode
            return pointerInputs[0] != Input.SELECT;
        }


    }

    public void undo() {
        Input input = inputManager.getInput(currentDevice);
        state.undo(context, input);
        renderCached();
    }

    private void redo() {
        Input input = inputManager.getInput(currentDevice);
        state.redo(context, input);
        renderCached();
    }

    public void doObjectEraser(Line eraserLine) {
        if (!isObjectEraserDoing) {
            isObjectEraserDoing = true;

            if (eraserLine != null) {
                List<Integer> deletedList = new ArrayList<>();
                List<BaseItem> deletedItems = new ArrayList<>();
                List<Integer> sizeList = new ArrayList<>();
                for (int ii = state.getItems().size() - 1; ii >= 0; ii--) {
                    BaseItem baseItem = state.getItems().get(ii);
                    if (baseItem instanceof Line) {
                        if (checkIntersect((Line) baseItem, eraserLine)) {
                            state.getItems().remove(baseItem);
                            deletedList.add(ii);
                            deletedItems.add(baseItem);
                            sizeList.add(0);
                        }
                    }
                }
                addEraserAction(deletedItems, deletedList, sizeList);
            }
            if (!cacheQueue.isEmpty()) {
                cacheQueue.clear();
            }
            renderCached();
            invalidateRender();
            isObjectEraserDoing = false;
        }
        notifyEraserDone = true;

    }

    public void doPartialEraser(Line eraserLine) {
        if (eraserLine == null) {
            log("doPartialEraser error : partialEraserPen == null");
            return;
        }
        log("do partial eraser.");
        if (!isPartialEraserDoing) {
            isPartialEraserDoing = true;
            List<BaseItem> erasedList = new ArrayList<>();
            List<Integer> deletedList = new ArrayList<>();
            List<Integer> sizeList = new ArrayList<>();
            List<BaseItem> deletedItems = new ArrayList<>();
            for (int ii = state.getItems().size() - 1; ii >= 0; ii--) {
                BaseItem baseItem = state.getItems().get(ii);

                if (baseItem instanceof Line) {
                    if (FastDraw.isSupportedCanvasFastDrawEraser()) {
                        eraserLine.updateAll();
                    }
                    if (checkIntersect((Line) baseItem, eraserLine)) {
                        List<BaseItem> temperasedList;
                        temperasedList = ((Line) baseItem).splitLine(eraserLine);
                        state.getItems().remove(baseItem);
                        if (temperasedList.size() > 0) {
                            erasedList.addAll(temperasedList);
                        }

                        if (erasedList.size() > 0) {
                            for (int i = 0; i < erasedList.size(); i++) {
                                if ((ii + i) >= state.getItems().size()) {
                                    state.getItems().add(erasedList.get(i));
                                } else {
                                    state.getItems().add(ii + i, erasedList.get(i));
                                }
                            }
                        }

                        deletedList.add(ii);
                        sizeList.add(erasedList.size());
                        deletedItems.add(baseItem);

                        erasedList.clear();
                    }
                }
            }

            addEraserAction(deletedItems, deletedList, sizeList);

            if (partialEraserPen != null) {
                partialEraserPen.clear();
                partialEraserPen = null;
            }

            if (listener != null) {
                listener.setMainThreadEraserAreaVisible(false, eraserSize, Input.PARTIAL_ERASER);
            }
            if (!cacheQueue.isEmpty()) {
                cacheQueue.clear();
            }
            renderCached();
            invalidateRender();
            isPartialEraserDoing = false;
        }
        notifyEraserDone = true;
    }

    private boolean checkIntersect(BaseItem item, Region region) {
        if (item instanceof Line) {
            Line line = (Line) item;
            for (int i = 0; i < line.getDetectRegions().size(); i++) {
                Region subRegion = new Region(line.getDetectRegions().get(i));
                if (subRegion.op(region, Region.Op.INTERSECT)) {
                    return true;
                }
            }
        } else {
//            Region subRegion = new Region();
            tempRegion.setEmpty();
            item.getRegion(tempRegion);
            if (tempRegion.op(region, Region.Op.INTERSECT)) {
                return true;
            }
        }
        return false;
    }

    //this function is for regions to regions check
    private boolean checkIntersect(Line line1, Line line2) {
        if (line1 == null || line2 == null)
            return false;
        if (line1.isEmpty() || line2.isEmpty())
            return false;
        for (int i = 0; i < line1.getDetectRegions().size(); i++) {
            Region subRegion1 = new Region(line1.getDetectRegions().get(i));
            for (int j = 0; j < line2.getDetectRegions().size(); j++) {
                Region subRegion2 = new Region(line2.getDetectRegions().get(j));
                if (subRegion2.op(subRegion1, Region.Op.INTERSECT)) {
                    return true;
                }
            }
        }
        return false;
    }


    private void updateSelectionBox() {
        log("updateSelectionBox");
        float left = width;
        float top = height;
        float right = 0;
        float bottom = 0;

        boolean imageOnly = true;

        if (selectedItems.isEmpty()) {
            selectionBox.hide();
            if (listener != null) {
                listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
            }
            return;
        }

        selectionBox.resetScaleLimit();
//        float downScaleLimit = selectionBox.getDownScaleLimit();
        for (BaseItem item : selectedItems) {
            left = Math.min(left, item.getLeft());
            right = Math.max(right, item.getRight());
            top = Math.min(top, item.getTop());
            bottom = Math.max(bottom, item.getBottom());
            if (imageOnly) {
                imageOnly = (item instanceof Image);
            }
            if (item instanceof Autoshape) {
                selectionBox.setDownScaleLimit(item.getWidth(), item.getHeight());
            }
        }

        final float width = right - left;
        final float height = bottom - top;
        selectionBox.setBounds(left, top, width, height);
        selectionBox.setRotation(0);
        selectionBox.setScale(0, 0);

        // resize selection preview bitmap

        Bitmap preview = selectionBox.getPreview();
        if (preview != null) {
            // increase size if selection box is larger
            if (preview.getWidth() < width || preview.getHeight() < height) {
                selectionBox.setPreview(Bitmap.createBitmap((int) width, (int) height,
                        Bitmap.Config.ARGB_8888));
                preview.recycle();
            }
        } else {
            // set initial size to size of whiteboard
            selectionBox.setPreview(Bitmap.createBitmap(this.width, this.height,
                    Bitmap.Config.ARGB_8888));
        }

        if (listener != null) {
            listener.onSelectionBoxImageOnly(imageOnly);
            listener.onSelectionBoxBoundsChange(left, top, right, bottom);
        }

        if (isItemTrimmable(selectedItems)) {
            switch (selectionBox.getSelectionRenderMode()) {
                case SelectionBox.ButtonMode.NORMAL: {
                    selectionBox.setSelectionRenderMode(SelectionBox.ButtonMode.TRIMMING);
                    break;
                }
            }
        } else {
            switch (selectionBox.getSelectionRenderMode()) {
                case SelectionBox.ButtonMode.TRIMMING: {
                    selectionBox.setSelectionRenderMode(SelectionBox.ButtonMode.NORMAL);
                    break;
                }
            }
        }

        boolean textWidthEditable = selectedItems.size() == 1 && selectedItems.get(0) instanceof Word;
        selectionBox.setTextScaleVisible(textWidthEditable);
        if (textWidthEditable) {
            Word word = (Word) selectedItems.get(0);
            selectionBox.updateScaleButtonBound(word.getTextBoxBounds(), word.getRotation());
            selectionBox.setShowSideHandles(false);
        }
    }

    private void updateHeadTailButton(SelectionBox selectionBox, float[] points) {
        if (selectedItems.size() == 1) {
            float[] p = ((Autoshape) selectedItems.get(0)).getPoints();
            selectionBox.updateHeadTailPosition(p[2], p[3], p[0], p[1]);
        }
    }

    public void update() {
        if (state != null) {
            selectionLine.clear();
            state.getSelectedItems(selectedItems);

            if (selectedItems.isEmpty()) {
                selectionBox.hide();
                if (listener != null) {
                    listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
                }
            } else {
                updateSelectionBox();

                selectionBox.setPreviewVisible(false);
                selectionBox.setOutlineVisible(true);
                if (listener != null) {
                    listener.onSelectionBoxVisibleChange(true, selectionBox.getSelectionRenderMode());
                }

                renderSelectionPreview();
            }

            renderCached();
        }
    }

    private void clearItems() {
        boolean isInputModeEraser = false;
        if (state != null) {
            int size = state.getItems().size();
            int[] deleteIds = new int[size];
            for (int i = 0; i < size; i++) {
                deleteIds[i] = i;
            }
            Action action = Action.createDeleteAction(deleteIds, state.getItems());
            state.addAction(action);

            state.removeItems(deleteIds);

            state.clearItems();
        }

        if (inputManager.getInput(Device.FINGER) == getLastEraserInput()) {
            inputManager.setInput(Device.FINGER, previousFingerInput);
            isInputModeEraser = true;
        }

        if (inputManager.getInput(Device.STYLUS) == getLastEraserInput()) {
            inputManager.setInput(Device.STYLUS, previousStylusInput);
            isInputModeEraser = true;
        }

        if (listener != null) {
            if (isInputModeEraser) {
                listener.onCurrentInputChange(currentDevice, currentDevice == Device.FINGER
                        ? previousFingerInput : previousStylusInput);
            }
        }

        update();
    }

    private void clearSelectedItems() {
        if (state != null) {
            state.clearSelected();
        }
        selectedItems.clear();

        selectionLine.clear();
        selectionBox.hide();
        selectionBox.setHeadTailVisible(false);
        if (listener != null) {
            listener.onSelectionBoxVisibleChange(false, selectionBox.getSelectionRenderMode());
        }

        renderCached();
    }

    // clear/set items and toggle keyboard when click the button on bottombar.
    private void clearTextBoxAndToggleKeyboard() {
//        listener.onSetActiveWord(false, null);
        boolean updateCache = false;
//        boolean updateSelectionBox = false;
        Input input = inputManager.getInput(currentDevice);
//        boolean isImageSearchVisible = (activity.findViewById(R.id.search_image_view).getVisibility()==View.VISIBLE);

        for (int i = 0; i < state.getItems().size(); i++) {
            BaseItem a = state.getItems().get(i);
            if (a instanceof Word) {
                Word w = (Word) a;
                if (!updateCache && w.getTextStatus().equals(Word.TextStatus.FOCUSED)) {
                    updateCache = true;
                    setActiveWord(null);
                }

//                if (!updateSelectionBox && w.getTextStatus().equals(Word.TextStatus.SELECTED)) {
//                    updateSelectionBox = true;
//                }

                w.setTextStatus(Word.TextStatus.UNFOCUSED);

                w.setSelectionAreaVisible(input == Input.TEXT);

                if (w.getText().length() <= 0) {
                    w.setTextStatus(Word.TextStatus.UNFOCUSED);// remove view from root
                    if (w.getEdited()) {
                        w.saveWordState();
                        Action textRemove = Action.createDeleteTextAction(i, w);
                        state.addAction(textRemove);
                    }
                    state.removeItem(w);
                }
            }
        }

//        if (updateSelectionBox) {
//            updateSelectionBox();
        if (updateCache) {
            renderCached();
        }
//        }

    }

    public void onBackButtonPressed() {
        if (activeWord != null) {
            activeWord.setSelectionAreaVisible(true);
        }
        clearTextBoxAndToggleKeyboard();
        update();
    }

    private void setTextSelectionAreaVisible(boolean visible) {
        for (BaseItem item : state.getItems()) {
            if (item instanceof Word) {
                ((Word) item).setSelectionAreaVisible(visible);
            }
        }
    }

    public void disableTrimmingMode() {
        selectionBox.setImageTrimming(false);
//        selectionMode = SelectionMode.DISABLE;
//        selectionMode = SelectionMode.NONE;
        isTrimmingActive = false;
    }

    public void setShowText(boolean b) {
        showTapScreen = b;
        if (b) {
            startBlinkTime = System.currentTimeMillis() - ADD_TEXT_BLINK_TIME;
        }
    }

    private int getBlinkAlpha(float max) {
        final long time = System.currentTimeMillis() - startBlinkTime;
        final float x = Math.abs(time % ADD_TEXT_BLINK_TIME - ADD_TEXT_BLINK_TIME / 2.0f) / ADD_TEXT_BLINK_TIME * 2;
        return (int) (max * x * 255);
    }

    private void setActiveWord(Word word) {
        log("setActiveWord: word=" + (word != null));
        activeWord = word;
        if (word != null) {
            if (listener != null) {
                listener.onSetActiveWord(true, word.getConfig());
            }
        } else {
            if (listener != null) {
                listener.onSetActiveWord(false, null);
            }
        }
    }

    private boolean isItemTrimmable(List<BaseItem> item) {
        if (item.size() != 1) {
            return false;
        }
        return !(item.get(0).getWidth() < TRIMMING_MIN_SIZE) && !(item.get(0).getHeight() < TRIMMING_MIN_SIZE) && (item.get(0) instanceof Image);
    }

    private void setCursor(int cur) {
        listener.setCursor(cur);
    }

    private void undoByEditText(String s, int keyCode) {
        if (activeWord == null) {
            return;
        }

        final int cursor = activeWord.getCursor();
        final int oldCursor = activeWord.getOldCursor();

        if (!activeWord.getEdited()) {
            activeWord.setEdited(true);
//            Action insert = Action.createInsertAction(mState.getItems().get(mState.getItems().size - 1).getId());
//            mState.addAction(insert);
            activeWord.quickSaveWordState();
            Action action = Action.createInsertAction(activeWord.getId());
            state.addAction(action);
            activeWord.setJustTouchDown(false);
            activeWord.setLastCharacter(s.charAt(oldCursor));

        } else if (activeWord.getLastText().equals(activeWord.getText())) {
            // Just skip it.
            // Sometimes onTextChanged() in textWatcher will be triggered but the text is as same as the previous text.
        } else if (activeWord.getJustTouchDown()) {
            activeWord.setJustTouchDown(false);
            activeWord.saveWordState();
            Action undoText = Action.createUndoTextAction(activeWord.getId());
            state.addAction(undoText);
            if (oldCursor - cursor == 1) {
                if (keyCode == KeyEvent.KEYCODE_DEL) {
                    activeWord.setLastCharacter(127);
                } else {
                    activeWord.setLastCharacter(-1);
                }
            } else if (cursor - oldCursor == 1) {
                activeWord.setLastCharacter(s.charAt(oldCursor));
            } else {
                activeWord.setLastCharacter(-1);
            }
        } else {
            if (Math.abs(cursor - oldCursor) > 1) {
                activeWord.saveWordState();
                Action undoText = Action.createUndoTextAction(activeWord.getId());
                state.addAction(undoText);
                activeWord.setLastCharacter(-1);

            } else if (cursor - oldCursor == 0) {
                activeWord.saveWordState();
                Action undoText = Action.createUndoTextAction(activeWord.getId());
                state.addAction(undoText);
                activeWord.setLastCharacter(-1);
            } else {//abs(cursor-oldCursor)==1
                final int lastCharacter = activeWord.getLastCharacter();
                if (cursor == oldCursor - 1) { // KeyEvent.KEYCODE_DEL == 67
                    if (keyCode != KeyEvent.KEYCODE_DEL) {//replace
                        activeWord.saveWordState();
                        Action undoText = Action.createUndoTextAction(activeWord.getId());
                        state.addAction(undoText);
                        activeWord.setLastCharacter(-1);
                    } else {

                        if (lastCharacter != 127) {// delete's ascii code is 127
                            activeWord.saveWordState();
                            Action undoText = Action.createUndoTextAction(activeWord.getId());
                            state.addAction(undoText);
                        }
                        activeWord.setLastCharacter(127);
                    }
                } else {
                    final int currentChar = s.charAt(oldCursor);
                    if (lastCharacter == '\r' || lastCharacter == '\n') {
                        if (currentChar != '\r' && currentChar != '\n') {
                            activeWord.saveWordState();
                            Action undoText = Action.createUndoTextAction(activeWord.getId());
                            state.addAction(undoText);
                        }
                        activeWord.setLastCharacter(currentChar);
                    } else {
                        if (lastCharacter == -1 ||
                                Character.isLetterOrDigit(lastCharacter) != Character.isLetterOrDigit(currentChar)) {
                            activeWord.saveWordState();
                            Action undoText = Action.createUndoTextAction(activeWord.getId());
                            state.addAction(undoText);
                        }
                        activeWord.setLastCharacter(currentChar);
                    }
                }
            }
        }
    }

    public boolean isTrimmingActive() {
        return Props.getPhase() >= Props.Phase.THREE && isTrimmingActive;
    }

    public void resetEraserPen() {
//        if(objectEraserPen != null) {
//            objectEraserPen.clear();
//            objectEraserPen = null;
//        }
//        if(partialEraserPen != null) {
//            partialEraserPen.clear();
//            partialEraserPen = null;
//        }
//        eraserSize = ERASER_SIZE;
        eraserSize = getEraserSize(Device.FINGER);
    }

//    public float getEraserSize() {
//        if(Props.isFake4k())
//            return context.getResources().getDimensionPixelSize(R.dimen.oem_eraser_circle_default_diameter)*2;
//        return context.getResources().getDimensionPixelSize(R.dimen.oem_eraser_circle_default_diameter);
//    }
//
//    private float getEraserSizePalm() {
//        if(Props.isFake4k())
//            return context.getResources().getDimensionPixelSize(R.dimen.oem_eraser_circle_palm_diameter)*2;
//        return context.getResources().getDimensionPixelSize(R.dimen.oem_eraser_circle_palm_diameter);
//    }

    public float getEraserSize(Device device) {
        float factor = DisplayDpiHelper.getInstance().getFake4kFactor();
        return (device == Device.PALM ? context.getResources().getDimensionPixelSize(R.dimen.oem_eraser_circle_palm_diameter) :
                context.getResources().getDimensionPixelSize(R.dimen.oem_eraser_circle_default_diameter)) * factor;
    }

    private void pushEnd() {
        List<BaseItem> items = state.getItems();

        if (selectedItems.size() == items.size()) {
            clearSelectedItems();
            renderCached();
            return;
        }

        int[] indexes = new int[selectedItems.size()];
        boolean alreadyBottom = true;

        for (int i = 0; i < selectedItems.size(); i++) {
            if (!items.get(i).equals(selectedItems.get(i))) {
                alreadyBottom = false;
                break;
            }
        }

        if (!alreadyBottom) {
            for (int i = items.size() - 1, j = selectedItems.size() - 1; i >= 0 && j >= 0; i--) {
                if (items.get(i).equals(selectedItems.get(j))) {
                    indexes[j] = i;
                    items.remove(i);
                    j--;
                }
            }

            List<BaseItem> buffer = new ArrayList<>(selectedItems);

            buffer.addAll(items);
            items.clear();
            items.addAll(buffer);

            Action pushEndAction = Action.createPushEndAction(indexes);
            state.addAction(pushEndAction);
        }

        clearSelectedItems();
        renderCached();
    }

    public boolean isClipboardEmpty() {
//        return scrapbook.isEmpty();
        List<BaseItem> clipboard = (state.isAnnotationState() ? annotationScrapbook : scrapbook);
        return clipboard.isEmpty();
    }

    public boolean isItemExists() {
        return state.getItems().size() > 0;
    }

    public boolean isPasteAllowed() {
        return !selectionBox.isVisible();
    }

    private void scrollTransform(PointF p) {
        tempPair[0] = p.x;
        tempPair[1] = p.y;

        scrollMatrix.reset();
        scrollMatrix.setScale(1 / zoomScale, 1 / zoomScale, zoomPoint.x, zoomPoint.y);
        scrollMatrix.mapPoints(tempPair);

        p.set(tempPair[0], tempPair[1]);
    }


    private final GestureDetector.SimpleOnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener() {
        @Override
        public boolean onDown(MotionEvent event) {
            return isMultiFingerActive;
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            float velocity = (float) Math.sqrt(velocityX * velocityX + velocityY * velocityY);
            if (isMultiFingerActive
                    && (velocity >= MIN_FLING_VELOCITY)) {
                scrollFlingStart = System.currentTimeMillis();
                flingHelper.setVelocity(velocityX / 1000, velocityY / 1000);
                isFlinging = true;
            }
            return false;
        }
    };

    private void addEraserAction(List<BaseItem> deletedItems, List<Integer> deletedList, List<Integer> sizeList) {
        if (deletedItems.size() > 0) {
            int[] deletedArray = new int[deletedList.size()];
            for (int i = 0; i < deletedList.size(); i++) {
                deletedArray[i] = deletedList.get(i);
            }
            int[] sizeArray = new int[sizeList.size()];
            for (int i = 0; i < sizeList.size(); i++) {
                sizeArray[i] = sizeList.get(i);
            }
            Action action = Action.createEraserAction(deletedArray, sizeArray, deletedItems);
            state.addAction(action);
        }
    }

    public Input getLastEraserInput() {
        return inputManager.getInput(Device.PALM);
    }


    /**
     * Types of input modes.
     */
    public enum Input {
        //        AUTOSHAPE, OBJECT_ERASER, HIGHLIGHTER, PEN, SELECT, TEXT, SCROLL, MOVE_CANVAS
        AUTOSHAPE, OBJECT_ERASER, HIGHLIGHTER, PEN, SELECT, TEXT, STICKYNOTE, MOVE_CANVAS, PARTIAL_ERASER
    }

    private enum SelectionMode {
        NONE, MOVE, ROTATE, ROTATE_AND_SCALE, SCALE, DISABLE, TEXT_SCALE, TRIMMING, REDIRECT
    }

    public interface Listener {
        void onSelectionBoxBoundsChange(float left, float top, float right, float bottom);

        void onSelectionBoxVisibleChange(boolean visible, int selectionRenderMode);

        void onCurrentInputChange(Device device, Input input);

        void onSetActiveWord(boolean visible, Word.Config config);

        void setCursor(int cur);

        void disableImageSearchTouchBlock();

        void cancelSearchTaskAfterAddImage();

        void onSelectionBoxImageOnly(boolean imageOnly);

        boolean isLongPressing();

        void updateStateJsonFile();

        void setMainThreadEraserAreaVisible(boolean visible, float size, Input input);

        void setMainThreadEraserPosition(PointF p);

        void reDrawViewOnCanvas(Canvas canvas, int width, int height, Paint paint);

        void onEraserRenderOver();

        void renderBackgroundFinished();

        void loadRulerRecord();
    }

    private static void log(String msg) {
        Log.d(TAG, msg);
    }

    public float getAppliedScale() {
        return this.state.getAppliedScale();
    }
}
